From 55c65e0a55488a21a14548ff9aa49e7e256eb4dc Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Thu, 25 Apr 2024 14:24:16 -0700 Subject: [PATCH 1/2] ci: add slack notifications nightly/develop failures (#2078) * ci: add slack notifications nightly/develop failures * event_name --- .github/workflows/build.yml | 9 +++++++++ .github/workflows/execute_advanced_tests.yaml | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8dfedb6f6a..2307fb52ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,6 +139,15 @@ jobs: exit 1 fi + - name: Notify Slack on Failure + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/develop' + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,commit,author,action,eventName,ref,workflow,job,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_CI_ALERTS }} + - name: Stop Private Network if: always() run: | diff --git a/.github/workflows/execute_advanced_tests.yaml b/.github/workflows/execute_advanced_tests.yaml index a4e77e3bf3..e66953ee28 100644 --- a/.github/workflows/execute_advanced_tests.yaml +++ b/.github/workflows/execute_advanced_tests.yaml @@ -47,6 +47,15 @@ jobs: docker logs -f "${container_id}" & exit $(docker wait "${container_id}") + - name: Notify Slack on Failure + if: failure() && github.event_name == 'schedule' + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,commit,author,action,eventName,ref,workflow,job,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_CI_ALERTS }} + e2e-upgrade-test: if: ${{ github.event.inputs.e2e-upgrade-test == 'true' || github.event_name == 'schedule' }} runs-on: buildjet-4vcpu-ubuntu-2204 @@ -64,6 +73,15 @@ jobs: docker logs -f "${container_id}" & exit $(docker wait "${container_id}") + - name: Notify Slack on Failure + if: failure() && github.event_name == 'schedule' + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,commit,author,action,eventName,ref,workflow,job,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_CI_ALERTS }} + e2e-upgrade-test-light: if: ${{ github.event.inputs.e2e-upgrade-test-light == 'true' }} runs-on: buildjet-4vcpu-ubuntu-2204 From 3e8f2e6621185723c13f2fbcacf8396ae8211910 Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Fri, 26 Apr 2024 15:25:04 +0200 Subject: [PATCH 2/2] refactor(`zetaclient`): improve some general structure of the codebase (#2032) * bitcoin * bitcoin 2 * config * evm * testutil and supply checker * tss and zetabridge * Update zetaclient/bitcoin/bitcoin_client.go Co-authored-by: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> * Update zetaclient/evm/evm_client.go Co-authored-by: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> * comments * changelog * change comments * review comments * fix tests --------- Co-authored-by: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> --- changelog.md | 6 + zetaclient/bitcoin/bitcoin_client.go | 830 ++++++++++-------- zetaclient/bitcoin/bitcoin_client_db_test.go | 3 +- zetaclient/bitcoin/bitcoin_client_test.go | 2 +- zetaclient/bitcoin/bitcoin_signer.go | 11 +- zetaclient/bitcoin/bitcoin_test.go | 5 +- zetaclient/bitcoin/fee.go | 13 +- zetaclient/bitcoin/inbound_tracker.go | 15 + zetaclient/bitcoin/tx_script.go | 26 +- zetaclient/common/logger.go | 2 + zetaclient/compliance/compliance.go | 26 +- zetaclient/config/config.go | 3 + zetaclient/config/config_chain.go | 8 - zetaclient/core_context/zeta_core_context.go | 10 + zetaclient/evm/constant.go | 5 + zetaclient/evm/evm_client.go | 756 +++++++++++----- zetaclient/evm/evm_signer.go | 228 ++--- zetaclient/evm/inbounds.go | 11 +- zetaclient/interfaces/interfaces.go | 1 - zetaclient/interfaces/signer.go | 8 +- zetaclient/keys/keys.go | 47 +- zetaclient/keys/keys_test.go | 19 +- zetaclient/metrics/burn_rate.go | 17 +- zetaclient/metrics/telemetry.go | 46 +- zetaclient/supplychecker/logger.go | 29 + zetaclient/supplychecker/validate.go | 30 + .../supplychecker/zeta_supply_checker.go | 73 +- zetaclient/testutils/constant.go | 24 +- zetaclient/testutils/testdata.go | 75 +- ...ager.go => concurrent_keysigns_tracker.go} | 0 ...go => concurrent_keysigns_tracker_test.go} | 0 zetaclient/tss/tss_signer.go | 53 +- zetaclient/tss/tss_signer_test.go | 17 +- zetaclient/types/account_resp.go | 12 - zetaclient/types/ethish_test.go | 1 - zetaclient/zetabridge/block_height.go | 24 - zetaclient/zetabridge/query_test.go | 6 - zetaclient/zetabridge/zetacore_bridge.go | 15 - zetaclient/zetacore_observer.go | 116 +-- 39 files changed, 1559 insertions(+), 1014 deletions(-) create mode 100644 zetaclient/supplychecker/logger.go create mode 100644 zetaclient/supplychecker/validate.go rename zetaclient/tss/{tss_keysign_manager.go => concurrent_keysigns_tracker.go} (100%) rename zetaclient/tss/{tss_keysign_manager_test.go => concurrent_keysigns_tracker_test.go} (100%) delete mode 100644 zetaclient/types/account_resp.go delete mode 100644 zetaclient/types/ethish_test.go delete mode 100644 zetaclient/zetabridge/block_height.go diff --git a/changelog.md b/changelog.md index 19c08aebc9..80054959a8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # CHANGELOG +## Unreleased + +### Refactor + +* [2032](https://github.com/zeta-chain/node/pull/2032) - improve some general structure of the ZetaClient codebase + ## Unreleased ### Breaking Changes diff --git a/zetaclient/bitcoin/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index 801171a88c..4f57bc2a7a 100644 --- a/zetaclient/bitcoin/bitcoin_client.go +++ b/zetaclient/bitcoin/bitcoin_client.go @@ -43,51 +43,100 @@ import ( ) const ( - DynamicDepositorFeeHeight = 834500 // The starting height (Bitcoin mainnet) from which dynamic depositor fee will take effect - maxHeightDiff = 10000 // in case the last block is too old when the observer starts - btcBlocksPerDay = 144 // for LRU block cache size - bigValueSats = 200000000 // 2 BTC - bigValueConfirmationCount = 6 // 6 confirmations for value >= 2 BTC + // DynamicDepositorFeeHeight contains the starting height (Bitcoin mainnet) from which dynamic depositor fee will take effect + DynamicDepositorFeeHeight = 834500 + + // maxHeightDiff contains the max height diff in case the last block is too old when the observer starts + maxHeightDiff = 10000 + + // 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 + + // bigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations + bigValueConfirmationCount = 6 ) var _ interfaces.ChainClient = &BTCChainClient{} -type BTCLog struct { - Chain zerolog.Logger // The parent logger for the chain - InTx zerolog.Logger // The logger for incoming transactions - OutTx zerolog.Logger // The logger for outgoing transactions - UTXOS zerolog.Logger // The logger for UTXOs management - GasPrice zerolog.Logger // The logger for gas price +// BTCLogger contains list of loggers used by Bitcoin chain client +// TODO: Merge this logger with the one in evm +// https://github.com/zeta-chain/node/issues/2022 +type BTCLogger struct { + // Chain is the parent logger for the chain + Chain zerolog.Logger + + // InTx is the logger for incoming transactions + InTx zerolog.Logger // The logger for incoming transactions + + // OutTx is the logger for outgoing transactions + OutTx zerolog.Logger // The logger for outgoing transactions + + // 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 } +// BTCInTxEvent represents an incoming transaction event +type BTCInTxEvent struct { + // FromAddress is the first input address + FromAddress string + + // ToAddress is the TSS address + ToAddress string + + // Value is the amount of BTC + Value float64 + + MemoBytes []byte + BlockNumber uint64 + TxHash string +} + // BTCChainClient represents a chain configuration for Bitcoin // Filled with above constants depending on chain type BTCChainClient struct { + // BlockTime contains the block time in seconds + BlockTime uint64 + + BlockCache *lru.Cache + + // Mu is lock for all the maps, utxos and core params + Mu *sync.Mutex + + Tss interfaces.TSSSigner + chain chains.Chain netParams *chaincfg.Params rpcClient interfaces.BTCRPCClient zetaClient interfaces.ZetaCoreBridger - Tss interfaces.TSSSigner lastBlock int64 lastBlockScanned int64 - BlockTime uint64 // block time in seconds + pendingNonce uint64 + utxos []btcjson.ListUnspentResult + params observertypes.ChainParams + coreContext *corecontext.ZetaCoreContext + + // includedTxHashes indexes included tx with tx hash + includedTxHashes map[string]bool + + // includedTxResults indexes tx results with the outbound tx identifier + includedTxResults map[string]*btcjson.GetTransactionResult - Mu *sync.Mutex // lock for all the maps, utxos and core params - pendingNonce uint64 - includedTxHashes map[string]bool // key: tx hash - includedTxResults map[string]*btcjson.GetTransactionResult // key: chain-tss-nonce - broadcastedTx map[string]string // key: chain-tss-nonce, value: outTx hash - utxos []btcjson.ListUnspentResult - params observertypes.ChainParams - coreContext *corecontext.ZetaCoreContext + // broadcastedTx indexes the outbound hash with the outbound tx identifier + broadcastedTx map[string]string db *gorm.DB stop chan struct{} - logger BTCLog + logger BTCLogger ts *metrics.TelemetryServer - - BlockCache *lru.Cache } func (ob *BTCChainClient) WithZetaClient(bridge *zetabridge.ZetaCoreBridge) { @@ -99,7 +148,7 @@ func (ob *BTCChainClient) WithZetaClient(bridge *zetabridge.ZetaCoreBridge) { func (ob *BTCChainClient) WithLogger(logger zerolog.Logger) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.logger = BTCLog{ + ob.logger = BTCLogger{ Chain: logger, InTx: logger.With().Str("module", "WatchInTx").Logger(), OutTx: logger.With().Str("module", "WatchOutTx").Logger(), @@ -143,19 +192,24 @@ func NewBitcoinClient( btcCfg config.BTCConfig, ts *metrics.TelemetryServer, ) (*BTCChainClient, error) { + // initialize the BTCChainClient ob := BTCChainClient{ 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 := loggers.Std.With().Str("chain", chain.ChainName.String()).Logger() - ob.logger = BTCLog{ + ob.logger = BTCLogger{ Chain: chainLogger, InTx: chainLogger.With().Str("module", "WatchInTx").Logger(), OutTx: chainLogger.With().Str("module", "WatchOutTx").Logger(), @@ -170,12 +224,15 @@ func NewBitcoinClient( 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 - // initialize the Client + + // create the RPC client ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) connCfg := &rpcclient.ConnConfig{ Host: btcCfg.RPCHost, @@ -189,6 +246,8 @@ func NewBitcoinClient( if err != nil { return nil, fmt.Errorf("error creating rpc client: %s", err) } + + // try connection ob.rpcClient = client err = client.Ping() if err != nil { @@ -201,7 +260,7 @@ func NewBitcoinClient( return nil, err } - //Load btc chain client DB + // load btc chain client DB err = ob.loadDB(dbpath) if err != nil { return nil, err @@ -210,6 +269,7 @@ func NewBitcoinClient( return &ob, nil } +// Start starts the Go routine to observe the Bitcoin chain func (ob *BTCChainClient) Start() { ob.logger.Chain.Info().Msgf("BitcoinChainClient is starting") go ob.WatchInTx() // watch bitcoin chain for incoming txs and post votes to zetacore @@ -231,37 +291,44 @@ func (ob *BTCChainClient) WatchRPCStatus() { if !ob.GetChainParams().IsSupported { continue } + bn, err := ob.rpcClient.GetBlockCount() if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } + hash, err := ob.rpcClient.GetBlockHash(bn) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } + header, err := ob.rpcClient.GetBlockHeader(hash) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } + blockTime := header.Timestamp elapsedSeconds := time.Since(blockTime).Seconds() if elapsedSeconds > 1200 { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } + tssAddr := ob.Tss.BTCAddressWitnessPubkeyHash() res, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: can't list utxos of TSS address; wallet or loaded? TSS address is not imported? ") continue } + if len(res) == 0 { ob.logger.Chain.Error().Err(err).Msg("RPC status check: TSS address has no utxos; TSS address is not imported? ") continue } + 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: @@ -327,10 +394,11 @@ func (ob *BTCChainClient) WatchInTx() { ob.logger.InTx.Error().Err(err).Msg("error creating ticker") return } - defer ticker.Stop() + ob.logger.InTx.Info().Msgf("WatchInTx started for chain %d", ob.chain.ChainId) sampledLogger := ob.logger.InTx.Sample(&zerolog.BasicSampler{N: 10}) + for { select { case <-ticker.C(): @@ -350,41 +418,6 @@ func (ob *BTCChainClient) WatchInTx() { } } -func (ob *BTCChainClient) postBlockHeader(tip int64) error { - ob.logger.InTx.Info().Msgf("postBlockHeader: tip %d", tip) - bn := tip - res, err := ob.zetaClient.GetBlockHeaderChainState(ob.chain.ChainId) - if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { - bn = res.ChainState.LatestHeight + 1 - } - if bn > tip { - return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) - } - res2, err := ob.GetBlockByNumberCached(bn) - if err != nil { - return fmt.Errorf("error getting bitcoin block %d: %s", bn, err) - } - - var headerBuf bytes.Buffer - err = res2.Header.Serialize(&headerBuf) - if err != nil { // should never happen - ob.logger.InTx.Error().Err(err).Msgf("error serializing bitcoin block header: %d", bn) - return err - } - blockHash := res2.Header.BlockHash() - _, err = ob.zetaClient.PostVoteBlockHeader( - ob.chain.ChainId, - blockHash[:], - res2.Block.Height, - proofs.NewBitcoinHeader(headerBuf.Bytes()), - ) - ob.logger.InTx.Info().Msgf("posted block header %d: %s", bn, blockHash) - if err != nil { // error shouldn't block the process - ob.logger.InTx.Error().Err(err).Msgf("error posting bitcoin block header: %d", bn) - } - return err -} - func (ob *BTCChainClient) ObserveInTx() error { // get and update latest block height cnt, err := ob.rpcClient.GetBlockCount() @@ -413,34 +446,41 @@ func (ob *BTCChainClient) ObserveInTx() error { } // query incoming gas asset to TSS address - { - bn := lastScanned + 1 - res, err := ob.GetBlockByNumberCached(bn) + blockNumber := lastScanned + 1 + res, err := ob.GetBlockByNumberCached(blockNumber) + if err != nil { + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error getting bitcoin block %d", blockNumber) + return err + } + ob.logger.InTx.Info().Msgf("observeInTxBTC: block %d has %d txs, current block %d, last block %d", + blockNumber, len(res.Block.Tx), cnt, lastScanned) + + // add block header to zetabridge + // TODO: consider having a separate ticker(from TSS scaning) for posting block headers + // https://github.com/zeta-chain/node/issues/1847 + flags := ob.coreContext.GetCrossChainFlags() + if flags.BlockHeaderVerificationFlags != nil && flags.BlockHeaderVerificationFlags.IsBtcTypeChainEnabled { + err = ob.postBlockHeader(blockNumber) if err != nil { - ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error getting bitcoin block %d", bn) - return err + ob.logger.InTx.Warn().Err(err).Msgf("observeInTxBTC: error posting block header %d", blockNumber) } - ob.logger.InTx.Info().Msgf("observeInTxBTC: block %d has %d txs, current block %d, last block %d", - bn, len(res.Block.Tx), cnt, lastScanned) + } - // print some debug information - if len(res.Block.Tx) > 1 { - for idx, tx := range res.Block.Tx { - ob.logger.InTx.Debug().Msgf("BTC InTX | %d: %s\n", idx, tx.Txid) - for vidx, vout := range tx.Vout { - ob.logger.InTx.Debug().Msgf("vout %d \n value: %v\n scriptPubKey: %v\n", vidx, vout.Value, vout.ScriptPubKey.Hex) - } - } - } + if len(res.Block.Tx) > 1 { + // get depositor fee + depositorFee := CalcDepositorFee(res.Block, ob.chain.ChainId, ob.netParams, ob.logger.InTx) + + // filter incoming txs to TSS address + tssAddress := ob.Tss.BTCAddress() // add block header to zetabridge // TODO: consider having a separate ticker(from TSS scaning) for posting block headers // https://github.com/zeta-chain/node/issues/1847 verificationFlags := ob.coreContext.GetVerificationFlags() if verificationFlags.BtcTypeChainEnabled { - err = ob.postBlockHeader(bn) + err = ob.postBlockHeader(blockNumber) if err != nil { - ob.logger.InTx.Warn().Err(err).Msgf("observeInTxBTC: error posting block header %d", bn) + ob.logger.InTx.Warn().Err(err).Msgf("observeInTxBTC: error posting block header %d", blockNumber) } } @@ -461,7 +501,7 @@ func (ob *BTCChainClient) ObserveInTx() error { depositorFee, ) if err != nil { - ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error filtering incoming txs for block %d", bn) + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error filtering incoming txs for block %d", blockNumber) return err // we have to re-scan this block next time } @@ -482,13 +522,47 @@ func (ob *BTCChainClient) ObserveInTx() error { } // Save LastBlockHeight - ob.SetLastBlockHeightScanned(bn) + ob.SetLastBlockHeightScanned(blockNumber) + // #nosec G701 always positive - if err := ob.db.Save(clienttypes.ToLastBlockSQLType(uint64(bn))).Error; err != nil { - ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error writing last scanned block %d to db", bn) + inTxs, err := FilterAndParseIncomingTx( + ob.rpcClient, + res.Block.Tx, + uint64(res.Block.Height), + tssAddress, + ob.logger.InTx, + ob.netParams, + depositorFee, + ) + if err != nil { + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error filtering incoming txs for block %d", blockNumber) + return err // we have to re-scan this block next time + } + + // post inbound vote message to zetacore + for _, inTx := range inTxs { + msg := ob.GetInboundVoteMessageFromBtcEvent(inTx) + if msg != nil { + zetaHash, ballot, err := ob.zetaClient.PostVoteInbound(zetabridge.PostVoteInboundGasLimit, zetabridge.PostVoteInboundExecutionGasLimit, msg) + if err != nil { + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error posting to zeta core for tx %s", inTx.TxHash) + return err // we have to re-scan this block next time + } else if zetaHash != "" { + ob.logger.InTx.Info().Msgf("observeInTxBTC: PostVoteInbound zeta tx hash: %s inTx %s ballot %s fee %v", + zetaHash, inTx.TxHash, ballot, depositorFee) + } + } } } + // Save LastBlockHeight + ob.SetLastBlockHeightScanned(blockNumber) + + // #nosec G701 always positive + if err := ob.db.Save(clienttypes.ToLastBlockSQLType(uint64(blockNumber))).Error; err != nil { + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error writing last scanned block %d to db", blockNumber) + } + return nil } @@ -500,6 +574,7 @@ func (ob *BTCChainClient) ConfirmationsThreshold(amount *big.Int) int64 { if bigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { return bigValueConfirmationCount } + // #nosec G701 always in range return int64(ob.GetChainParams().ConfirmationCount) } @@ -577,6 +652,7 @@ func (ob *BTCChainClient) IsOutboundProcessed(cctx *types.CrossChainTx, logger z } else if zetaHash != "" { logger.Info().Msgf("IsOutboundProcessed: confirmed Bitcoin outTx %s, zeta tx hash %s nonce %d ballot %s", res.TxID, zetaHash, nonce, ballot) } + return true, true, nil } @@ -618,20 +694,20 @@ func (ob *BTCChainClient) WatchGasPrice() { func (ob *BTCChainClient) PostGasPrice() error { if ob.chain.ChainId == 18444 { //bitcoin regtest; hardcode here since this RPC is not available on regtest - bn, err := ob.rpcClient.GetBlockCount() + blockNumber, err := ob.rpcClient.GetBlockCount() if err != nil { return err } + // #nosec G701 always in range - zetaHash, err := ob.zetaClient.PostGasPrice(ob.chain, 1, "100", uint64(bn)) + _, err = ob.zetaClient.PostGasPrice(ob.chain, 1, "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err } - _ = zetaHash - //ob.logger.WatchGasPrice.Debug().Msgf("PostGasPrice zeta tx: %s", zetaHash) return nil } + // EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation feeResult, err := ob.rpcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) if err != nil { @@ -644,27 +720,20 @@ func (ob *BTCChainClient) PostGasPrice() error { return fmt.Errorf("gas price is too large: %f", *feeResult.FeeRate) } feeRatePerByte := FeeRateToSatPerByte(*feeResult.FeeRate) - bn, err := ob.rpcClient.GetBlockCount() + + blockNumber, err := ob.rpcClient.GetBlockCount() if err != nil { return err } + // #nosec G701 always positive - zetaHash, err := ob.zetaClient.PostGasPrice(ob.chain, feeRatePerByte.Uint64(), "100", uint64(bn)) + _, err = ob.zetaClient.PostGasPrice(ob.chain, feeRatePerByte.Uint64(), "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err } - _ = zetaHash - return nil -} -type BTCInTxEvnet struct { - FromAddress string // the first input address - ToAddress string // some TSS address - Value float64 // in BTC, not satoshi - MemoBytes []byte - BlockNumber uint64 - TxHash string + return nil } // FilterAndParseIncomingTx given txs list returned by the "getblock 2" RPC command, return the txs that are relevant to us @@ -679,17 +748,19 @@ func FilterAndParseIncomingTx( logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, -) ([]*BTCInTxEvnet, error) { - inTxs := make([]*BTCInTxEvnet, 0) +) ([]*BTCInTxEvent, error) { + inTxs := make([]*BTCInTxEvent, 0) for idx, tx := range txs { if idx == 0 { continue // the first tx is coinbase; we do not process coinbase tx } + inTx, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, depositorFee) if err != nil { // unable to parse the tx, the caller should retry return nil, errors.Wrapf(err, "error getting btc event for tx %s in block %d", tx.Txid, blockNumber) } + if inTx != nil { inTxs = append(inTxs, inTx) logger.Info().Msgf("FilterAndParseIncomingTx: found btc event for tx %s in block %d", tx.Txid, blockNumber) @@ -698,7 +769,7 @@ func FilterAndParseIncomingTx( return inTxs, nil } -func (ob *BTCChainClient) GetInboundVoteMessageFromBtcEvent(inTx *BTCInTxEvnet) *types.MsgVoteOnObservedInboundTx { +func (ob *BTCChainClient) GetInboundVoteMessageFromBtcEvent(inTx *BTCInTxEvent) *types.MsgVoteOnObservedInboundTx { ob.logger.InTx.Debug().Msgf("Processing inTx: %s", inTx.TxHash) amount := big.NewFloat(inTx.Value) amount = amount.Mul(amount, big.NewFloat(1e8)) @@ -706,6 +777,7 @@ func (ob *BTCChainClient) GetInboundVoteMessageFromBtcEvent(inTx *BTCInTxEvnet) message := hex.EncodeToString(inTx.MemoBytes) // compliance check + // if the inbound contains restricted addresses, return nil if ob.IsInTxRestricted(inTx) { return nil } @@ -729,7 +801,7 @@ func (ob *BTCChainClient) GetInboundVoteMessageFromBtcEvent(inTx *BTCInTxEvnet) } // IsInTxRestricted returns true if the inTx contains restricted addresses -func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvnet) bool { +func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvent) bool { receiver := "" parsedAddress, _, err := chains.ParseAddressAndData(hex.EncodeToString(inTx.MemoBytes)) if err == nil && parsedAddress != (ethcommon.Address{}) { @@ -753,7 +825,7 @@ func GetBtcEvent( logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, -) (*BTCInTxEvnet, error) { +) (*BTCInTxEvent, error) { found := false var value float64 var memo []byte @@ -761,15 +833,18 @@ func GetBtcEvent( // 1st vout must have tss address as receiver with p2wpkh scriptPubKey vout0 := tx.Vout[0] script := vout0.ScriptPubKey.Hex - if len(script) == 44 && script[:4] == "0014" { // P2WPKH output: 0x00 + 20 bytes of pubkey hash + if len(script) == 44 && script[:4] == "0014" { + // P2WPKH output: 0x00 + 20 bytes of pubkey hash receiver, err := DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) if err != nil { // should never happen return nil, err } + // skip irrelevant tx to us if receiver != tssAddress { return nil, nil } + // deposit amount has to be no less than the minimum depositor fee if vout0.Value < depositorFee { logger.Info().Msgf("GetBtcEvent: btc deposit amount %v in txid %s is less than depositor fee %v", vout0.Value, tx.Txid, depositorFee) @@ -791,11 +866,13 @@ func GetBtcEvent( if len(tx.Vin) == 0 { // should never happen return nil, fmt.Errorf("GetBtcEvent: no input found for intx: %s", tx.Txid) } + fromAddress, err := GetSenderAddressByVin(rpcClient, tx.Vin[0], netParams) if err != nil { return nil, errors.Wrapf(err, "error getting sender address for intx: %s", tx.Txid) } - return &BTCInTxEvnet{ + + return &BTCInTxEvent{ FromAddress: fromAddress, ToAddress: tssAddress, Value: value, @@ -815,10 +892,12 @@ func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, n if err != nil { return "", err } + tx, err := rpcClient.GetRawTransaction(hash) if err != nil { return "", errors.Wrapf(err, "error getting raw transaction %s", vin.Txid) } + // #nosec G701 - always in range if len(tx.MsgTx().TxOut) <= int(vin.Vout) { return "", fmt.Errorf("vout index %d out of range for tx %s", vin.Vout, vin.Txid) @@ -842,6 +921,7 @@ func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, n if IsPkScriptP2PKH(pkScript) { return DecodeScriptP2PKH(scriptHex, net) } + // sender address not found, return nil and move on to the next tx return "", nil } @@ -935,92 +1015,6 @@ func (ob *BTCChainClient) FetchUTXOS() error { 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 *BTCChainClient) isTssTransaction(txid string) bool { - _, found := ob.includedTxHashes[txid] - return found -} - -// refreshPendingNonce tries increasing the artificial pending nonce of outTx (if lagged behind). -// There could be many (unpredictable) reasons for a pending nonce lagging behind, for example: -// 1. The zetaclient gets restarted. -// 2. The tracker is missing in zetabridge. -func (ob *BTCChainClient) refreshPendingNonce() { - // get pending nonces from zetabridge - p, err := ob.zetaClient.GetPendingNoncesByChain(ob.chain.ChainId) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") - } - - // increase pending nonce if lagged behind - ob.Mu.Lock() - pendingNonce := ob.pendingNonce - ob.Mu.Unlock() - - // #nosec G701 always non-negative - nonceLow := uint64(p.NonceLow) - if nonceLow > pendingNonce { - // get the last included outTx hash - txid, err := ob.getOutTxidByNonce(nonceLow-1, false) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting last outTx txid") - } - - // set 'NonceLow' as the new pending nonce - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.pendingNonce = nonceLow - ob.logger.Chain.Info().Msgf("refreshPendingNonce: increase pending nonce to %d with txid %s", ob.pendingNonce, txid) - } -} - -func (ob *BTCChainClient) getOutTxidByNonce(nonce uint64, test bool) (string, error) { - - // There are 2 types of txids an observer can trust - // 1. The ones had been verified and saved by observer self. - // 2. The ones had been finalized in zetabridge based on majority vote. - if res := ob.getIncludedTx(nonce); res != nil { - return res.TxID, nil - } - if !test { // if not unit test, get cctx from zetabridge - send, err := ob.zetaClient.GetCctxByNonce(ob.chain.ChainId, nonce) - if err != nil { - return "", errors.Wrapf(err, "getOutTxidByNonce: error getting cctx for nonce %d", nonce) - } - txid := send.GetCurrentOutTxParam().OutboundTxHash - if txid == "" { - return "", fmt.Errorf("getOutTxidByNonce: cannot find outTx txid for nonce %d", nonce) - } - // make sure it's a real Bitcoin txid - _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) - if err != nil { - return "", errors.Wrapf(err, "getOutTxidByNonce: error getting outTx result for nonce %d hash %s", nonce, txid) - } - if getTxResult.Confirmations <= 0 { // just a double check - return "", fmt.Errorf("getOutTxidByNonce: outTx txid %s for nonce %d is not included", txid, nonce) - } - return txid, nil - } - return "", fmt.Errorf("getOutTxidByNonce: cannot find outTx txid for nonce %d", nonce) -} - -func (ob *BTCChainClient) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { - tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() - amount := chains.NonceMarkAmount(nonce) - for i, utxo := range ob.utxos { - sats, err := GetSatoshis(utxo.Amount) - if err != nil { - ob.logger.OutTx.Error().Err(err).Msgf("findNonceMarkUTXO: error getting satoshis for utxo %v", utxo) - } - if utxo.Address == tssAddress && sats == amount && utxo.TxID == txid && utxo.Vout == 0 { - ob.logger.OutTx.Info().Msgf("findNonceMarkUTXO: found nonce-mark utxo with txid %s, amount %d satoshi", utxo.TxID, sats) - return i, nil - } - } - return -1, fmt.Errorf("findNonceMarkUTXO: cannot find nonce-mark utxo with nonce %d", nonce) -} - // SelectUTXOs selects a sublist of utxos to be used as inputs. // // Parameters: @@ -1035,7 +1029,13 @@ func (ob *BTCChainClient) findNonceMarkUTXO(nonce uint64, txid string) (int, err // - the total value of the selected UTXOs. // - the number of consolidated UTXOs. // - the total value of the consolidated UTXOs. -func (ob *BTCChainClient) SelectUTXOs(amount float64, utxosToSpend uint16, nonce uint64, consolidateRank uint16, test bool) ([]btcjson.ListUnspentResult, float64, uint16, float64, error) { +func (ob *BTCChainClient) SelectUTXOs( + amount float64, + utxosToSpend uint16, + nonce uint64, + consolidateRank uint16, + test bool, +) ([]btcjson.ListUnspentResult, float64, uint16, float64, error) { idx := -1 if nonce == 0 { // for nonce = 0; make exception; no need to include nonce-mark utxo @@ -1128,10 +1128,11 @@ func (ob *BTCChainClient) WatchOutTx() { ob.logger.OutTx.Error().Err(err).Msg("error creating ticker ") return } - defer ticker.Stop() + ob.logger.OutTx.Info().Msgf("WatchInTx started for chain %d", ob.chain.ChainId) sampledLogger := ob.logger.OutTx.Sample(&zerolog.BasicSampler{N: 10}) + for { select { case <-ticker.C(): @@ -1152,14 +1153,17 @@ func (ob *BTCChainClient) WatchOutTx() { ob.logger.OutTx.Info().Err(err).Msgf("WatchOutTx: can't find cctx for chain %d nonce %d", ob.chain.ChainId, tracker.Nonce) break } + nonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce if tracker.Nonce != nonce { // Tanmay: it doesn't hurt to check ob.logger.OutTx.Error().Msgf("WatchOutTx: tracker nonce %d not match cctx nonce %d", tracker.Nonce, nonce) break } + if len(tracker.HashList) > 1 { ob.logger.OutTx.Warn().Msgf("WatchOutTx: oops, outTxID %s got multiple (%d) outTx hashes", outTxID, len(tracker.HashList)) } + // iterate over all txHashes to find the truly included one. // we do it this (inefficient) way because we don't rely on the first one as it may be a false positive (for unknown reason). txCount := 0 @@ -1176,6 +1180,7 @@ func (ob *BTCChainClient) WatchOutTx() { } } } + if txCount == 1 { // should be only one txHash included for each nonce ob.setIncludedTx(tracker.Nonce, txResult) } else if txCount > 1 { @@ -1191,42 +1196,288 @@ func (ob *BTCChainClient) WatchOutTx() { } } -// checkIncludedTx checks if a txHash is included and returns (txResult, inMempool) -// Note: if txResult is nil, then inMempool flag should be ignored. -func (ob *BTCChainClient) checkIncludedTx(cctx *types.CrossChainTx, txHash string) (*btcjson.GetTransactionResult, bool) { - outTxID := ob.GetTxID(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) - hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) +// GetTxResultByHash gets the transaction result by hash +func GetTxResultByHash(rpcClient interfaces.BTCRPCClient, txID string) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { + hash, err := chainhash.NewHashFromStr(txID) if err != nil { - ob.logger.OutTx.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) - return nil, false - } - if txHash != getTxResult.TxID { // just in case, we'll use getTxResult.TxID later - ob.logger.OutTx.Error().Msgf("checkIncludedTx: inconsistent txHash %s and getTxResult.TxID %s", txHash, getTxResult.TxID) - return nil, false + return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error NewHashFromStr: %s", txID) } - if getTxResult.Confirmations >= 0 { // check included tx only - err = ob.checkTssOutTxResult(cctx, hash, getTxResult) - if err != nil { - ob.logger.OutTx.Error().Err(err).Msgf("checkIncludedTx: error verify bitcoin outTx %s outTxID %s", txHash, outTxID) - return nil, false - } - return getTxResult, false // included + + // The Bitcoin node has to be configured to watch TSS address + txResult, err := rpcClient.GetTransaction(hash) + if err != nil { + return nil, nil, errors.Wrapf(err, "GetOutTxByTxHash: error GetTransaction %s", hash.String()) } - return getTxResult, true // in mempool + return hash, txResult, nil } -// setIncludedTx saves included tx result in memory -func (ob *BTCChainClient) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransactionResult) { - txHash := getTxResult.TxID - outTxID := ob.GetTxID(nonce) - - ob.Mu.Lock() - defer ob.Mu.Unlock() - res, found := ob.includedTxResults[outTxID] - - if !found { // not found. - ob.includedTxHashes[txHash] = true - ob.includedTxResults[outTxID] = getTxResult // include new outTx and enforce rigid 1-to-1 mapping: nonce <===> txHash +// GetRawTxResult gets the raw tx result +func GetRawTxResult(rpcClient interfaces.BTCRPCClient, hash *chainhash.Hash, res *btcjson.GetTransactionResult) (btcjson.TxRawResult, error) { + if res.Confirmations == 0 { // for pending tx, we query the raw tx directly + rawResult, err := rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx + if err != nil { + return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetRawTransactionVerbose %s", res.TxID) + } + return *rawResult, nil + } else if res.Confirmations > 0 { // for confirmed tx, we query the block + blkHash, err := chainhash.NewHashFromStr(res.BlockHash) + if err != nil { + return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error NewHashFromStr for block hash %s", res.BlockHash) + } + block, err := rpcClient.GetBlockVerboseTx(blkHash) + if err != nil { + return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetBlockVerboseTx %s", res.BlockHash) + } + if res.BlockIndex < 0 || res.BlockIndex >= int64(len(block.Tx)) { + return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: invalid outTx with invalid block index, TxID %s, BlockIndex %d", res.TxID, res.BlockIndex) + } + return block.Tx[res.BlockIndex], nil + } + + // res.Confirmations < 0 (meaning not included) + return btcjson.TxRawResult{}, fmt.Errorf("getRawTxResult: tx %s not included yet", hash) +} + +func (ob *BTCChainClient) BuildBroadcastedTxMap() error { + var broadcastedTransactions []clienttypes.OutTxHashSQLType + 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 +} + +func (ob *BTCChainClient) LoadLastBlock() error { + bn, err := ob.rpcClient.GetBlockCount() + if err != nil { + return err + } + + //Load persisted block number + var lastBlockNum clienttypes.LastBlockSQLType + if err := ob.db.First(&lastBlockNum, clienttypes.LastBlockNumID).Error; err != nil { + ob.logger.Chain.Info().Msg("LastBlockNum not found in DB, scan from latest") + ob.SetLastBlockHeightScanned(bn) + } else { + // #nosec G701 always in range + lastBN := int64(lastBlockNum.Num) + ob.SetLastBlockHeightScanned(lastBN) + + //If persisted block number is too low, use the latest height + if (bn - lastBN) > maxHeightDiff { + ob.logger.Chain.Info().Msgf("LastBlockNum too low: %d, scan from latest", lastBlockNum.Num) + ob.SetLastBlockHeightScanned(bn) + } + } + + if ob.chain.ChainId == 18444 { // bitcoin regtest: start from block 100 + ob.SetLastBlockHeightScanned(100) + } + ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) + + return nil +} + +func (ob *BTCChainClient) GetTxID(nonce uint64) string { + tssAddr := ob.Tss.BTCAddress() + return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) +} + +type BTCBlockNHeader struct { + Header *wire.BlockHeader + Block *btcjson.GetBlockVerboseTxResult +} + +func (ob *BTCChainClient) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { + if result, ok := ob.BlockCache.Get(blockNumber); ok { + return result.(*BTCBlockNHeader), nil + } + // Get the block hash + hash, err := ob.rpcClient.GetBlockHash(blockNumber) + if err != nil { + return nil, err + } + // Get the block header + header, err := ob.rpcClient.GetBlockHeader(hash) + if err != nil { + return nil, err + } + // Get the block with verbose transactions + block, err := ob.rpcClient.GetBlockVerboseTx(hash) + if err != nil { + return nil, err + } + blockNheader := &BTCBlockNHeader{ + Header: header, + Block: block, + } + ob.BlockCache.Add(blockNumber, blockNheader) + ob.BlockCache.Add(hash, blockNheader) + return blockNheader, nil +} + +func (ob *BTCChainClient) postBlockHeader(tip int64) error { + ob.logger.InTx.Info().Msgf("postBlockHeader: tip %d", tip) + bn := tip + res, err := ob.zetaClient.GetBlockHeaderChainState(ob.chain.ChainId) + if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { + bn = res.ChainState.LatestHeight + 1 + } + if bn > tip { + return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) + } + res2, err := ob.GetBlockByNumberCached(bn) + if err != nil { + return fmt.Errorf("error getting bitcoin block %d: %s", bn, err) + } + + var headerBuf bytes.Buffer + err = res2.Header.Serialize(&headerBuf) + if err != nil { // should never happen + ob.logger.InTx.Error().Err(err).Msgf("error serializing bitcoin block header: %d", bn) + return err + } + blockHash := res2.Header.BlockHash() + _, err = ob.zetaClient.PostVoteBlockHeader( + ob.chain.ChainId, + blockHash[:], + res2.Block.Height, + proofs.NewBitcoinHeader(headerBuf.Bytes()), + ) + ob.logger.InTx.Info().Msgf("posted block header %d: %s", bn, blockHash) + if err != nil { // error shouldn't block the process + ob.logger.InTx.Error().Err(err).Msgf("error posting bitcoin block header: %d", bn) + } + return err +} + +// 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 *BTCChainClient) isTssTransaction(txid string) bool { + _, found := ob.includedTxHashes[txid] + return found +} + +// refreshPendingNonce tries increasing the artificial pending nonce of outTx (if lagged behind). +// There could be many (unpredictable) reasons for a pending nonce lagging behind, for example: +// 1. The zetaclient gets restarted. +// 2. The tracker is missing in zetabridge. +func (ob *BTCChainClient) refreshPendingNonce() { + // get pending nonces from zetabridge + p, err := ob.zetaClient.GetPendingNoncesByChain(ob.chain.ChainId) + if err != nil { + ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") + } + + // increase pending nonce if lagged behind + ob.Mu.Lock() + pendingNonce := ob.pendingNonce + ob.Mu.Unlock() + + // #nosec G701 always non-negative + nonceLow := uint64(p.NonceLow) + if nonceLow > pendingNonce { + // get the last included outTx hash + txid, err := ob.getOutTxidByNonce(nonceLow-1, false) + if err != nil { + ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting last outTx txid") + } + + // set 'NonceLow' as the new pending nonce + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.pendingNonce = nonceLow + ob.logger.Chain.Info().Msgf("refreshPendingNonce: increase pending nonce to %d with txid %s", ob.pendingNonce, txid) + } +} + +func (ob *BTCChainClient) getOutTxidByNonce(nonce uint64, test bool) (string, error) { + + // There are 2 types of txids an observer can trust + // 1. The ones had been verified and saved by observer self. + // 2. The ones had been finalized in zetabridge based on majority vote. + if res := ob.getIncludedTx(nonce); res != nil { + return res.TxID, nil + } + if !test { // if not unit test, get cctx from zetabridge + send, err := ob.zetaClient.GetCctxByNonce(ob.chain.ChainId, nonce) + if err != nil { + return "", errors.Wrapf(err, "getOutTxidByNonce: error getting cctx for nonce %d", nonce) + } + txid := send.GetCurrentOutTxParam().OutboundTxHash + if txid == "" { + return "", fmt.Errorf("getOutTxidByNonce: cannot find outTx txid for nonce %d", nonce) + } + // make sure it's a real Bitcoin txid + _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) + if err != nil { + return "", errors.Wrapf(err, "getOutTxidByNonce: error getting outTx result for nonce %d hash %s", nonce, txid) + } + if getTxResult.Confirmations <= 0 { // just a double check + return "", fmt.Errorf("getOutTxidByNonce: outTx txid %s for nonce %d is not included", txid, nonce) + } + return txid, nil + } + return "", fmt.Errorf("getOutTxidByNonce: cannot find outTx txid for nonce %d", nonce) +} + +func (ob *BTCChainClient) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { + tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + amount := chains.NonceMarkAmount(nonce) + for i, utxo := range ob.utxos { + sats, err := GetSatoshis(utxo.Amount) + if err != nil { + ob.logger.OutTx.Error().Err(err).Msgf("findNonceMarkUTXO: error getting satoshis for utxo %v", utxo) + } + if utxo.Address == tssAddress && sats == amount && utxo.TxID == txid && utxo.Vout == 0 { + ob.logger.OutTx.Info().Msgf("findNonceMarkUTXO: found nonce-mark utxo with txid %s, amount %d satoshi", utxo.TxID, sats) + return i, nil + } + } + return -1, fmt.Errorf("findNonceMarkUTXO: cannot find nonce-mark utxo with nonce %d", nonce) +} + +// checkIncludedTx checks if a txHash is included and returns (txResult, inMempool) +// Note: if txResult is nil, then inMempool flag should be ignored. +func (ob *BTCChainClient) checkIncludedTx(cctx *types.CrossChainTx, txHash string) (*btcjson.GetTransactionResult, bool) { + outTxID := ob.GetTxID(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) + if err != nil { + ob.logger.OutTx.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) + return nil, false + } + + if txHash != getTxResult.TxID { // just in case, we'll use getTxResult.TxID later + ob.logger.OutTx.Error().Msgf("checkIncludedTx: inconsistent txHash %s and getTxResult.TxID %s", txHash, getTxResult.TxID) + return nil, false + } + + if getTxResult.Confirmations >= 0 { // check included tx only + err = ob.checkTssOutTxResult(cctx, hash, getTxResult) + if err != nil { + ob.logger.OutTx.Error().Err(err).Msgf("checkIncludedTx: error verify bitcoin outTx %s outTxID %s", txHash, outTxID) + return nil, false + } + return getTxResult, false // included + } + return getTxResult, true // in mempool +} + +// setIncludedTx saves included tx result in memory +func (ob *BTCChainClient) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransactionResult) { + txHash := getTxResult.TxID + outTxID := ob.GetTxID(nonce) + + ob.Mu.Lock() + defer ob.Mu.Unlock() + res, found := ob.includedTxResults[outTxID] + + if !found { // not found. + ob.includedTxHashes[txHash] = true + ob.includedTxResults[outTxID] = getTxResult // include new outTx and enforce rigid 1-to-1 mapping: nonce <===> txHash if nonce >= ob.pendingNonce { // try increasing pending nonce on every newly included outTx ob.pendingNonce = nonce + 1 } @@ -1293,48 +1544,6 @@ func (ob *BTCChainClient) checkTssOutTxResult(cctx *types.CrossChainTx, hash *ch return nil } -// GetTxResultByHash gets the transaction result by hash -func GetTxResultByHash(rpcClient interfaces.BTCRPCClient, txID string) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { - hash, err := chainhash.NewHashFromStr(txID) - if err != nil { - return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error NewHashFromStr: %s", txID) - } - - // The Bitcoin node has to be configured to watch TSS address - txResult, err := rpcClient.GetTransaction(hash) - if err != nil { - return nil, nil, errors.Wrapf(err, "GetOutTxByTxHash: error GetTransaction %s", hash.String()) - } - return hash, txResult, nil -} - -// GetRawTxResult gets the raw tx result -func GetRawTxResult(rpcClient interfaces.BTCRPCClient, hash *chainhash.Hash, res *btcjson.GetTransactionResult) (btcjson.TxRawResult, error) { - if res.Confirmations == 0 { // for pending tx, we query the raw tx directly - rawResult, err := rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetRawTransactionVerbose %s", res.TxID) - } - return *rawResult, nil - } else if res.Confirmations > 0 { // for confirmed tx, we query the block - blkHash, err := chainhash.NewHashFromStr(res.BlockHash) - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error NewHashFromStr for block hash %s", res.BlockHash) - } - block, err := rpcClient.GetBlockVerboseTx(blkHash) - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetBlockVerboseTx %s", res.BlockHash) - } - if res.BlockIndex < 0 || res.BlockIndex >= int64(len(block.Tx)) { - return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: invalid outTx with invalid block index, TxID %s, BlockIndex %d", res.TxID, res.BlockIndex) - } - return block.Tx[res.BlockIndex], nil - } - - // res.Confirmations < 0 (meaning not included) - return btcjson.TxRawResult{}, fmt.Errorf("getRawTxResult: tx %s not included yet", hash) -} - // checkTSSVin checks vin is valid if: // - The first input is the nonce-mark // - All inputs are from TSS address @@ -1449,49 +1658,6 @@ func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, return nil } -func (ob *BTCChainClient) BuildBroadcastedTxMap() error { - var broadcastedTransactions []clienttypes.OutTxHashSQLType - 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 -} - -func (ob *BTCChainClient) LoadLastBlock() error { - bn, err := ob.rpcClient.GetBlockCount() - if err != nil { - return err - } - - //Load persisted block number - var lastBlockNum clienttypes.LastBlockSQLType - if err := ob.db.First(&lastBlockNum, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.Chain.Info().Msg("LastBlockNum not found in DB, scan from latest") - ob.SetLastBlockHeightScanned(bn) - } else { - // #nosec G701 always in range - lastBN := int64(lastBlockNum.Num) - ob.SetLastBlockHeightScanned(lastBN) - - //If persisted block number is too low, use the latest height - if (bn - lastBN) > maxHeightDiff { - ob.logger.Chain.Info().Msgf("LastBlockNum too low: %d, scan from latest", lastBlockNum.Num) - ob.SetLastBlockHeightScanned(bn) - } - } - - if ob.chain.ChainId == 18444 { // bitcoin regtest: start from block 100 - ob.SetLastBlockHeightScanned(100) - } - ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) - - return nil -} - func (ob *BTCChainClient) loadDB(dbpath string) error { if _, err := os.Stat(dbpath); os.IsNotExist(err) { err := os.MkdirAll(dbpath, os.ModePerm) @@ -1524,41 +1690,3 @@ func (ob *BTCChainClient) loadDB(dbpath string) error { return err } - -func (ob *BTCChainClient) GetTxID(nonce uint64) string { - tssAddr := ob.Tss.BTCAddress() - return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) -} - -type BTCBlockNHeader struct { - Header *wire.BlockHeader - Block *btcjson.GetBlockVerboseTxResult -} - -func (ob *BTCChainClient) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { - if result, ok := ob.BlockCache.Get(blockNumber); ok { - return result.(*BTCBlockNHeader), nil - } - // Get the block hash - hash, err := ob.rpcClient.GetBlockHash(blockNumber) - if err != nil { - return nil, err - } - // Get the block header - header, err := ob.rpcClient.GetBlockHeader(hash) - if err != nil { - return nil, err - } - // Get the block with verbose transactions - block, err := ob.rpcClient.GetBlockVerboseTx(hash) - if err != nil { - return nil, err - } - blockNheader := &BTCBlockNHeader{ - Header: header, - Block: block, - } - ob.BlockCache.Add(blockNumber, blockNheader) - ob.BlockCache.Add(hash, blockNheader) - return blockNheader, nil -} diff --git a/zetaclient/bitcoin/bitcoin_client_db_test.go b/zetaclient/bitcoin/bitcoin_client_db_test.go index 2f4a58943f..751713b4d7 100644 --- a/zetaclient/bitcoin/bitcoin_client_db_test.go +++ b/zetaclient/bitcoin/bitcoin_client_db_test.go @@ -58,8 +58,7 @@ func (suite *BitcoinClientDBTestSuite) SetupTest() { } } -func (suite *BitcoinClientDBTestSuite) TearDownSuite() { -} +func (suite *BitcoinClientDBTestSuite) TearDownSuite() {} func (suite *BitcoinClientDBTestSuite) TestSubmittedTx() { var submittedTransactions []clienttypes.TransactionResultSQLType diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index dfa576c9c1..82c7d69917 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -512,7 +512,7 @@ func TestGetBtcEvent(t *testing.T) { // expected result memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) require.NoError(t, err) - eventExpected := &BTCInTxEvnet{ + eventExpected := &BTCInTxEvent{ FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", ToAddress: tssAddress, Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index a5ff22fa6a..246f6b4de7 100644 --- a/zetaclient/bitcoin/bitcoin_signer.go +++ b/zetaclient/bitcoin/bitcoin_signer.go @@ -38,6 +38,8 @@ const ( consolidationRank = 10 ) +var _ interfaces.ChainSigner = &BTCSigner{} + // BTCSigner deals with signing BTC transactions and implements the ChainSigner interface type BTCSigner struct { tssSigner interfaces.TSSSigner @@ -48,8 +50,6 @@ type BTCSigner struct { coreContext *corecontext.ZetaCoreContext } -var _ interfaces.ChainSigner = &BTCSigner{} - func NewBTCSigner( cfg config.BTCConfig, tssSigner interfaces.TSSSigner, @@ -247,11 +247,12 @@ func (signer *BTCSigner) SignWithdrawTx( return nil, err } } - tss, ok := signer.tssSigner.(*tss.TSS) + + tssSigner, ok := signer.tssSigner.(*tss.TSS) if !ok { return nil, fmt.Errorf("tssSigner is not a TSS") } - sig65Bs, err := tss.SignBatch(witnessHashes, height, nonce, chain) + sig65Bs, err := tssSigner.SignBatch(witnessHashes, height, nonce, chain) if err != nil { return nil, fmt.Errorf("SignBatch error: %v", err) } @@ -270,6 +271,7 @@ func (signer *BTCSigner) SignWithdrawTx( txWitness := wire.TxWitness{append(sig.Serialize(), byte(hashType)), pkCompressed} tx.TxIn[ix].Witness = txWitness } + return tx, nil } @@ -288,6 +290,7 @@ func (signer *BTCSigner) Broadcast(signedTx *wire.MsgTx) error { if err != nil { return err } + signer.logger.Info().Msgf("Broadcasting BTC tx , hash %s ", hash) return nil } diff --git a/zetaclient/bitcoin/bitcoin_test.go b/zetaclient/bitcoin/bitcoin_test.go index 4dae7f8590..5bd13a0d31 100644 --- a/zetaclient/bitcoin/bitcoin_test.go +++ b/zetaclient/bitcoin/bitcoin_test.go @@ -29,9 +29,7 @@ type BTCSignTestSuite struct { const ( prevOut = "07a84f4bd45a633e93871be5c98d958afd13a37f3cf5010f40eec0840d19f5fa" - // tb1q7r6lnqjhvdjuw9uf4ehx7fs0euc6cxnqz7jj50 - pk = "cQkjdfeMU8vHvE6jErnFVqZYYZnGGYy64jH6zovbSXdfTjte6QgY" - utxoCount = 5 + pk = "cQkjdfeMU8vHvE6jErnFVqZYYZnGGYy64jH6zovbSXdfTjte6QgY" ) func (suite *BTCSignTestSuite) SetupTest() { @@ -70,7 +68,6 @@ func (suite *BTCSignTestSuite) TestSign() { suite.T().Logf("wallet signed tx : %v\n", walletSignedTX) // sign tx using tss signature - tssSignedTX, err := getTSSTX(suite.testSigner, tx, txSigHashes, idx, amt, subscript, txscript.SigHashAll) suite.Require().NoError(err) suite.T().Logf("tss signed tx : %v\n", tssSignedTX) diff --git a/zetaclient/bitcoin/fee.go b/zetaclient/bitcoin/fee.go index ab19240264..7b184021aa 100644 --- a/zetaclient/bitcoin/fee.go +++ b/zetaclient/bitcoin/fee.go @@ -36,13 +36,13 @@ const ( ) var ( - // The outtx size incurred by the depositor: 68vB + // BtcOutTxBytesDepositor is the outtx size incurred by the depositor: 68vB BtcOutTxBytesDepositor = OuttxSizeDepositor() - // The outtx size incurred by the withdrawer: 177vB + // BtcOutTxBytesWithdrawer is the outtx size incurred by the withdrawer: 177vB BtcOutTxBytesWithdrawer = OuttxSizeWithdrawer() - // The default depositor fee is 0.00001360 BTC (20 * 68vB / 100000000) + // DefaultDepositorFee is the default depositor fee is 0.00001360 BTC (20 * 68vB / 100000000) // default depositor fee calculation is based on a fixed fee rate of 20 sat/byte just for simplicity. DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) ) @@ -82,8 +82,10 @@ func EstimateOuttxSize(numInputs uint64, payees []btcutil.Address) (uint64, erro } bytesToPayees += sizeOutput } + // calculate the size of the witness bytesWitness := bytes1stWitness + (numInputs-1)*bytesPerWitness + // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations // Calculation for signed SegWit tx: blockchain.GetTransactionWeight(tx) / 4 return bytesWiredTx + bytesInput + bytesOutput + bytesToPayees + bytesWitness/blockchain.WitnessScaleFactor, nil @@ -133,6 +135,7 @@ func OuttxSizeWithdrawer() uint64 { bytesInput := uint64(1) * bytesPerInput // nonce mark bytesOutput := uint64(2) * bytesPerOutputP2WPKH // 2 P2WPKH outputs: new nonce mark, change bytesOutput += bytesPerOutputAvg // 1 output to withdrawer's address + return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor } @@ -151,6 +154,7 @@ func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *ch if len(blockVb.Tx) == 1 { return 0, nil // only coinbase tx, it happens } + txCoinbase := &blockVb.Tx[0] if blockVb.Weight < blockchain.WitnessScaleFactor { return 0, fmt.Errorf("block weight %d too small", blockVb.Weight) @@ -200,6 +204,7 @@ func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *ch // calculate average fee rate. vBytes := txsWeight / blockchain.WitnessScaleFactor + return txsFees / int64(vBytes), nil } @@ -220,7 +225,9 @@ func CalcDepositorFee(blockVb *btcjson.GetBlockVerboseTxResult, chainID int64, n feeRate = defaultDepositorFeeRate // use default fee rate if calculation fails, should not happen logger.Error().Err(err).Msgf("cannot calculate fee rate for block %d", blockVb.Height) } + // #nosec G701 always in range feeRate = int64(float64(feeRate) * clientcommon.BTCOuttxGasPriceMultiplier) + return DepositorFee(feeRate) } diff --git a/zetaclient/bitcoin/inbound_tracker.go b/zetaclient/bitcoin/inbound_tracker.go index e0dbd595cb..c1e97c14d2 100644 --- a/zetaclient/bitcoin/inbound_tracker.go +++ b/zetaclient/bitcoin/inbound_tracker.go @@ -38,11 +38,13 @@ func (ob *BTCChainClient) WatchIntxTracker() { } } +// ObserveTrackerSuggestions checks for inbound tracker suggestions func (ob *BTCChainClient) ObserveTrackerSuggestions() error { trackers, err := ob.zetaClient.GetInboundTrackersForChain(ob.chain.ChainId) if err != nil { return err } + for _, tracker := range trackers { ob.logger.InTx.Info().Msgf("checking tracker with hash :%s and coin-type :%s ", tracker.TxHash, tracker.CoinType) ballotIdentifier, err := ob.CheckReceiptForBtcTxHash(tracker.TxHash, true) @@ -51,49 +53,61 @@ func (ob *BTCChainClient) ObserveTrackerSuggestions() error { } ob.logger.InTx.Info().Msgf("Vote submitted for inbound Tracker,Chain : %s,Ballot Identifier : %s, coin-type %s", ob.chain.ChainName, ballotIdentifier, coin.CoinType_Gas.String()) } + return nil } +// CheckReceiptForBtcTxHash checks the receipt for a btc tx hash func (ob *BTCChainClient) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, error) { hash, err := chainhash.NewHashFromStr(txHash) if err != nil { return "", err } + tx, err := ob.rpcClient.GetRawTransactionVerbose(hash) if err != nil { return "", err } + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) if err != nil { return "", err } + blockVb, err := ob.rpcClient.GetBlockVerboseTx(blockHash) if err != nil { return "", err } + if len(blockVb.Tx) <= 1 { return "", fmt.Errorf("block %d has no transactions", blockVb.Height) } + depositorFee := CalcDepositorFee(blockVb, ob.chain.ChainId, ob.netParams, ob.logger.InTx) tss, err := ob.zetaClient.GetBtcTssAddress(ob.chain.ChainId) if err != nil { return "", err } + // #nosec G701 always positive event, err := GetBtcEvent(ob.rpcClient, *tx, tss, uint64(blockVb.Height), ob.logger.InTx, ob.netParams, depositorFee) if err != nil { return "", err } + if event == nil { return "", errors.New("no btc deposit event found") } + msg := ob.GetInboundVoteMessageFromBtcEvent(event) if msg == nil { return "", errors.New("no message built for btc sent to TSS") } + if !vote { return msg.Digest(), nil } + zetaHash, ballot, err := ob.zetaClient.PostVoteInbound(zetabridge.PostVoteInboundGasLimit, zetabridge.PostVoteInboundExecutionGasLimit, msg) if err != nil { ob.logger.InTx.Error().Err(err).Msg("error posting to zeta core") @@ -102,5 +116,6 @@ func (ob *BTCChainClient) CheckReceiptForBtcTxHash(txHash string, vote bool) (st ob.logger.InTx.Info().Msgf("BTC deposit detected and reported: PostVoteInbound zeta tx hash: %s inTx %s ballot %s fee %v", zetaHash, txHash, ballot, depositorFee) } + return msg.Digest(), nil } diff --git a/zetaclient/bitcoin/tx_script.go b/zetaclient/bitcoin/tx_script.go index 08046b0171..f68fc0e224 100644 --- a/zetaclient/bitcoin/tx_script.go +++ b/zetaclient/bitcoin/tx_script.go @@ -18,19 +18,19 @@ import ( ) const ( - // Lenth of P2TR script [OP_1 0x20 <32-byte-hash>] + // LengthScriptP2TR is the lenth of P2TR script [OP_1 0x20 <32-byte-hash>] LengthScriptP2TR = 34 - // Length of P2WSH script [OP_0 0x20 <32-byte-hash>] + // LengthScriptP2WSH is the length of P2WSH script [OP_0 0x20 <32-byte-hash>] LengthScriptP2WSH = 34 - // Length of P2WPKH script [OP_0 0x14 <20-byte-hash>] + // LengthScriptP2WPKH is the length of P2WPKH script [OP_0 0x14 <20-byte-hash>] LengthScriptP2WPKH = 22 - // Length of P2SH script [OP_HASH160 0x14 <20-byte-hash> OP_EQUAL] + // LengthScriptP2SH is the length of P2SH script [OP_HASH160 0x14 <20-byte-hash> OP_EQUAL] LengthScriptP2SH = 23 - // Length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] + // LengthScriptP2PKH is the length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] LengthScriptP2PKH = 25 ) @@ -87,11 +87,13 @@ func DecodeScriptP2TR(scriptHex string, net *chaincfg.Params) (string, error) { if !IsPkScriptP2TR(script) { return "", fmt.Errorf("invalid P2TR script: %s", scriptHex) } + witnessProg := script[2:] receiverAddress, err := chains.NewAddressTaproot(witnessProg, net) if err != nil { // should never happen return "", errors.Wrapf(err, "error getting address from script %s", scriptHex) } + return receiverAddress.EncodeAddress(), nil } @@ -104,11 +106,13 @@ func DecodeScriptP2WSH(scriptHex string, net *chaincfg.Params) (string, error) { if !IsPkScriptP2WSH(script) { return "", fmt.Errorf("invalid P2WSH script: %s", scriptHex) } + witnessProg := script[2:] receiverAddress, err := btcutil.NewAddressWitnessScriptHash(witnessProg, net) if err != nil { // should never happen return "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) } + return receiverAddress.EncodeAddress(), nil } @@ -121,11 +125,13 @@ func DecodeScriptP2WPKH(scriptHex string, net *chaincfg.Params) (string, error) if !IsPkScriptP2WPKH(script) { return "", fmt.Errorf("invalid P2WPKH script: %s", scriptHex) } + witnessProg := script[2:] receiverAddress, err := btcutil.NewAddressWitnessPubKeyHash(witnessProg, net) if err != nil { // should never happen return "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) } + return receiverAddress.EncodeAddress(), nil } @@ -138,7 +144,9 @@ func DecodeScriptP2SH(scriptHex string, net *chaincfg.Params) (string, error) { if !IsPkScriptP2SH(script) { return "", fmt.Errorf("invalid P2SH script: %s", scriptHex) } + scriptHash := script[2:22] + return EncodeAddress(scriptHash, net.ScriptHashAddrID), nil } @@ -151,7 +159,9 @@ func DecodeScriptP2PKH(scriptHex string, net *chaincfg.Params) (string, error) { if !IsPkScriptP2PKH(script) { return "", fmt.Errorf("invalid P2PKH script: %s", scriptHex) } + pubKeyHash := script[3:23] + return EncodeAddress(pubKeyHash, net.PubKeyHashAddrID), nil } @@ -166,6 +176,7 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { if int(memoSize) != (len(scriptHex)-4)/2 { return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(scriptHex)-4)/2) } + memoBytes, err := hex.DecodeString(scriptHex[4:]) if err != nil { return nil, false, errors.Wrapf(err, "error hex decoding memo: %s", scriptHex) @@ -175,6 +186,7 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { } return memoBytes, true, nil } + return nil, false, nil } @@ -196,16 +208,19 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai if err != nil { return "", 0, errors.Wrap(err, "error getting satoshis") } + // get btc chain params chainParams, err := chains.GetBTCChainParams(chain.ChainId) if err != nil { return "", 0, errors.Wrapf(err, "error GetBTCChainParams for chain %d", chain.ChainId) } + // decode cctx receiver address addr, err := chains.DecodeBtcAddress(receiverExpected, chain.ChainId) if err != nil { return "", 0, errors.Wrapf(err, "error decoding receiver %s", receiverExpected) } + // parse receiver address from vout var receiverVout string switch addr.(type) { @@ -225,5 +240,6 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai if err != nil { return "", 0, errors.Wrap(err, "error decoding TSS vout") } + return receiverVout, amount, nil } diff --git a/zetaclient/common/logger.go b/zetaclient/common/logger.go index 251f4cf774..6098b67084 100644 --- a/zetaclient/common/logger.go +++ b/zetaclient/common/logger.go @@ -5,11 +5,13 @@ import ( "github.com/rs/zerolog/log" ) +// ClientLogger is a struct that contains the logger for a chain client type ClientLogger struct { Std zerolog.Logger Compliance zerolog.Logger } +// DefaultLoggers returns the default loggers for a chain client func DefaultLoggers() ClientLogger { return ClientLogger{ Std: log.Logger, diff --git a/zetaclient/compliance/compliance.go b/zetaclient/compliance/compliance.go index 4bfd08fdf5..db7db6bcc4 100644 --- a/zetaclient/compliance/compliance.go +++ b/zetaclient/compliance/compliance.go @@ -10,30 +10,34 @@ import ( func IsCctxRestricted(cctx *crosschaintypes.CrossChainTx) bool { sender := cctx.InboundTxParams.Sender receiver := cctx.GetCurrentOutTxParam().Receiver + return config.ContainRestrictedAddress(sender, receiver) } // PrintComplianceLog prints compliance log with fields [chain, cctx/intx, chain, sender, receiver, token] func PrintComplianceLog( - logger1 zerolog.Logger, - logger2 zerolog.Logger, + inboundLogger zerolog.Logger, + complianceLogger zerolog.Logger, outbound bool, chainID int64, - identifier, sender, receiver, token string) { + identifier, sender, receiver, token string, +) { var logMsg string - var logWithFields1 zerolog.Logger - var logWithFields2 zerolog.Logger + var inboundLoggerWithFields zerolog.Logger + var complianceLoggerWithFields zerolog.Logger + if outbound { // we print cctx for outbound tx logMsg = "Restricted address detected in cctx" - logWithFields1 = logger1.With().Int64("chain", chainID).Str("cctx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() - logWithFields2 = logger2.With().Int64("chain", chainID).Str("cctx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() + inboundLoggerWithFields = inboundLogger.With().Int64("chain", chainID).Str("cctx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() + complianceLoggerWithFields = complianceLogger.With().Int64("chain", chainID).Str("cctx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() } else { // we print intx for inbound tx logMsg = "Restricted address detected in intx" - logWithFields1 = logger1.With().Int64("chain", chainID).Str("intx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() - logWithFields2 = logger2.With().Int64("chain", chainID).Str("intx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() + inboundLoggerWithFields = inboundLogger.With().Int64("chain", chainID).Str("intx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() + complianceLoggerWithFields = complianceLogger.With().Int64("chain", chainID).Str("intx", identifier).Str("sender", sender).Str("receiver", receiver).Str("token", token).Logger() } - logWithFields1.Warn().Msg(logMsg) - logWithFields2.Warn().Msg(logMsg) + + inboundLoggerWithFields.Warn().Msg(logMsg) + complianceLoggerWithFields.Warn().Msg(logMsg) } diff --git a/zetaclient/config/config.go b/zetaclient/config/config.go index 4cb0e6b21b..8680071c3c 100644 --- a/zetaclient/config/config.go +++ b/zetaclient/config/config.go @@ -21,6 +21,7 @@ func Save(config *Config, path string) error { if err != nil { return err } + file := filepath.Join(path, folder, filename) file = filepath.Clean(file) @@ -28,10 +29,12 @@ func Save(config *Config, path string) error { if err != nil { return err } + err = os.WriteFile(file, jsonFile, 0600) if err != nil { return err } + return nil } diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index f211f1030d..bb414c6161 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -3,14 +3,6 @@ package config import "github.com/zeta-chain/zetacore/pkg/chains" const ( - BtcConfirmationCount = 1 - DevEthConfirmationCount = 2 - - // TssTestPrivkey is the private key of the TSS address - // #nosec G101 - used for testing only - TssTestPrivkey = "2082bc9775d6ee5a05ef221a9d1c00b3cc3ecb274a4317acc0a182bc1e05d1bb" - TssTestAddress = "0xE80B6467863EbF8865092544f441da8fD3cF6074" - MaxBlocksPerPeriod = 100 ) diff --git a/zetaclient/core_context/zeta_core_context.go b/zetaclient/core_context/zeta_core_context.go index 5bb8d23089..e602512fd6 100644 --- a/zetaclient/core_context/zeta_core_context.go +++ b/zetaclient/core_context/zeta_core_context.go @@ -35,11 +35,13 @@ func NewZetaCoreContext(cfg config.Config) *ZetaCoreContext { for _, e := range cfg.EVMChainConfigs { evmChainParams[e.Chain.ChainId] = &observertypes.ChainParams{} } + var bitcoinChainParams *observertypes.ChainParams _, found := cfg.GetBTCConfig() if found { bitcoinChainParams = &observertypes.ChainParams{} } + return &ZetaCoreContext{ coreContextLock: new(sync.RWMutex), chainsEnabled: []chains.Chain{}, @@ -59,6 +61,7 @@ func (c *ZetaCoreContext) GetKeygen() observertypes.Keygen { copiedPubkeys = make([]string, len(c.keygen.GranteePubkeys)) copy(copiedPubkeys, c.keygen.GranteePubkeys) } + return observertypes.Keygen{ Status: c.keygen.Status, GranteePubkeys: copiedPubkeys, @@ -107,10 +110,12 @@ func (c *ZetaCoreContext) GetBTCChainParams() (chains.Chain, *observertypes.Chai if c.bitcoinChainParams == nil { // bitcoin is not enabled return chains.Chain{}, &observertypes.ChainParams{}, false } + chain := chains.GetChainFromChainID(c.bitcoinChainParams.ChainId) if chain == nil { panic(fmt.Sprintf("BTCChain is missing for chainID %d", c.bitcoinChainParams.ChainId)) } + return *chain, c.bitcoinChainParams, true } @@ -146,6 +151,7 @@ func (c *ZetaCoreContext) Update( sort.SliceStable(newChains, func(i, j int) bool { return newChains[i].ChainId < newChains[j].ChainId }) + if len(newChains) == 0 { logger.Warn().Msg("UpdateChainParams: No chains enabled in ZeroCore") } @@ -170,16 +176,20 @@ func (c *ZetaCoreContext) Update( } } } + if keygen != nil { c.keygen = *keygen } + c.chainsEnabled = newChains c.crossChainFlags = crosschainFlags c.verificationFlags = verificationFlags + // update chain params for bitcoin if it has config in file if c.bitcoinChainParams != nil && btcChainParams != nil { c.bitcoinChainParams = btcChainParams } + // update core params for evm chains we have configs in file for _, params := range evmChainParams { _, found := c.evmChainParams[params.ChainId] diff --git a/zetaclient/evm/constant.go b/zetaclient/evm/constant.go index 8dcd2309d6..d3a22adef9 100644 --- a/zetaclient/evm/constant.go +++ b/zetaclient/evm/constant.go @@ -12,22 +12,27 @@ const ( // OutTxTrackerReportTimeout is the timeout for waiting for an outtx tracker report OutTxTrackerReportTimeout = 10 * time.Minute + // TopicsZetaSent is the number of topics for a Zeta sent event // [signature, zetaTxSenderAddress, destinationChainId] // https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ZetaConnector.base.sol#L34 TopicsZetaSent = 3 + // TopicsZetaReceived is the number of topics for a Zeta received event // [signature, sourceChainId, destinationAddress, internalSendHash] // https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ZetaConnector.base.sol#L45 TopicsZetaReceived = 4 + // TopicsZetaReverted is the number of topics for a Zeta reverted event // [signature, destinationChainId, internalSendHash] // https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ZetaConnector.base.sol#L54 TopicsZetaReverted = 3 + // TopicsWithdrawn is the number of topics for a withdrawn event // [signature, recipient, asset] // https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ERC20Custody.sol#L43 TopicsWithdrawn = 3 + // TopicsDeposited is the number of topics for a deposited event // [signature, asset] // https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ERC20Custody.sol#L42 TopicsDeposited = 2 diff --git a/zetaclient/evm/evm_client.go b/zetaclient/evm/evm_client.go index 63e8da7d7c..f86211fe60 100644 --- a/zetaclient/evm/evm_client.go +++ b/zetaclient/evm/evm_client.go @@ -13,6 +13,9 @@ import ( "sync/atomic" "time" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -31,35 +34,37 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/proofs" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" "github.com/zeta-chain/zetacore/zetaclient/interfaces" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" "github.com/zeta-chain/zetacore/zetaclient/zetabridge" - "gorm.io/driver/sqlite" - "gorm.io/gorm" ) -type TxHashEnvelope struct { - TxHash string - Done chan struct{} -} +// 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 -type OutTx struct { - SendHash string - TxHash string - Nonce int64 -} -type Log struct { - Chain zerolog.Logger // The parent logger for the chain - InTx zerolog.Logger // Logger for incoming trasnactions - OutTx zerolog.Logger // Logger for outgoing transactions - GasPrice zerolog.Logger // Logger for gas prices - Compliance zerolog.Logger // Logger for compliance checks + // InTx is the logger for incoming transactions + InTx zerolog.Logger + + // OutTx is the logger for outgoing transactions + OutTx zerolog.Logger + + // GasPrice is the logger for gas prices + GasPrice zerolog.Logger + + // Compliance is the logger for compliance checks + Compliance zerolog.Logger } var _ interfaces.ChainClient = &ChainClient{} @@ -67,22 +72,26 @@ var _ interfaces.ChainClient = &ChainClient{} // ChainClient represents the chain configuration for an EVM chain // Filled with above constants depending on chain type ChainClient struct { + Tss interfaces.TSSSigner + + // BlockTimeExternalChain is the block time of the external chain + BlockTimeExternalChain uint64 + + Mu *sync.Mutex + chain chains.Chain evmClient interfaces.EVMRPCClient evmJSONRPC interfaces.EVMJSONRPCClient zetaBridge interfaces.ZetaCoreBridger - Tss interfaces.TSSSigner lastBlockScanned uint64 lastBlock uint64 - BlockTimeExternalChain uint64 // block time in seconds txWatchList map[ethcommon.Hash]string - Mu *sync.Mutex db *gorm.DB outTxPendingTransactions map[string]*ethtypes.Transaction outTXConfirmedReceipts map[string]*ethtypes.Receipt outTXConfirmedTransactions map[string]*ethtypes.Transaction stop chan struct{} - logger Log + logger Logger coreContext *corecontext.ZetaCoreContext chainParams observertypes.ChainParams ts *metrics.TelemetryServer @@ -104,19 +113,22 @@ func NewEVMChainClient( ob := ChainClient{ ts: ts, } + chainLogger := loggers.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() - ob.logger = Log{ + ob.logger = Logger{ Chain: chainLogger, InTx: chainLogger.With().Str("module", "WatchInTx").Logger(), OutTx: chainLogger.With().Str("module", "WatchOutTx").Logger(), GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), Compliance: loggers.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 @@ -134,6 +146,7 @@ func NewEVMChainClient( ob.logger.Chain.Error().Err(err).Msg("eth Client Dial") return nil, err } + ob.evmClient = client ob.evmJSONRPC = ethrpc.NewEthRPC(evmCfg.Endpoint) @@ -143,6 +156,7 @@ func NewEVMChainClient( 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") @@ -170,7 +184,7 @@ func (ob *ChainClient) WithChain(chain chains.Chain) { func (ob *ChainClient) WithLogger(logger zerolog.Logger) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.logger = Log{ + ob.logger = Logger{ Chain: logger, InTx: logger.With().Str("module", "WatchInTx").Logger(), OutTx: logger.With().Str("module", "WatchOutTx").Logger(), @@ -262,11 +276,20 @@ func FetchERC20CustodyContract(addr ethcommon.Address, client interfaces.EVMRPCC // Start all observation routines for the evm chain func (ob *ChainClient) Start() { - go ob.WatchInTx() // watch evm chain for incoming txs and post votes to zetacore - go ob.WatchOutTx() // watch evm chain for outgoing txs status - go ob.WatchGasPrice() // watch evm chain for gas prices and post to zetacore - go ob.WatchIntxTracker() // watch zetacore for intx trackers - go ob.WatchRPCStatus() // watch the RPC status of the evm chain + // watch evm chain for incoming txs and post votes to zetacore + go ob.WatchInTx() + + // watch evm chain for outgoing txs status + go ob.WatchOutTx() + + // watch evm chain for gas prices and post to zetacore + go ob.WatchGasPrice() + + // watch zetacore for intx trackers + go ob.WatchIntxTracker() + + // watch the RPC status of the evm chain + go ob.WatchRPCStatus() } // WatchRPCStatus watches the RPC status of the evm chain @@ -325,6 +348,300 @@ func (ob *ChainClient) Stop() { ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) } +// IsSendOutTxProcessed returns isIncluded, isConfirmed, Error +// if isConfirmed, it also post to ZetaCore +func (ob *ChainClient) IsSendOutTxProcessed(cctx *crosschaintypes.CrossChainTx, logger zerolog.Logger) (bool, bool, error) { + sendHash := cctx.Index + cointype := cctx.InboundTxParams.CoinType + nonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce + + // skip if outtx is not confirmed + params := ob.GetChainParams() + receipt, transaction := ob.GetTxNReceipt(nonce) + if receipt == nil || transaction == nil { // not confirmed yet + return false, false, nil + } + + sendID := fmt.Sprintf("%s-%d", ob.chain.String(), nonce) + logger = logger.With().Str("sendID", sendID).Logger() + + // compliance check, special handling the cancelled cctx + if compliance.IsCctxRestricted(cctx) { + recvStatus := chains.ReceiveStatus_Failed + if receipt.Status == 1 { + recvStatus = chains.ReceiveStatus_Success + } + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendHash, + receipt.TxHash.Hex(), + receipt.BlockNumber.Uint64(), + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + // use cctx's amount to bypass the amount check in zetacore + cctx.GetCurrentOutTxParam().Amount.BigInt(), + recvStatus, + ob.chain, + nonce, + coin.CoinType_Cmd, + ) + if err != nil { + logger.Error().Err(err).Msgf("error posting confirmation to meta core for cctx %s nonce %d", sendHash, nonce) + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } + + if cointype == coin.CoinType_Cmd { + recvStatus := chains.ReceiveStatus_Failed + if receipt.Status == 1 { + recvStatus = chains.ReceiveStatus_Success + } + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendHash, + receipt.TxHash.Hex(), + receipt.BlockNumber.Uint64(), + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + transaction.Value(), + recvStatus, + ob.chain, + nonce, + coin.CoinType_Cmd, + ) + if err != nil { + logger.Error().Err(err).Msgf("error posting confirmation to meta core for cctx %s nonce %d", sendHash, nonce) + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + + } else if cointype == coin.CoinType_Gas { // the outbound is a regular Ether/BNB/Matic transfer; no need to check events + if receipt.Status == 1 { + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendHash, + receipt.TxHash.Hex(), + receipt.BlockNumber.Uint64(), + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + transaction.Value(), + chains.ReceiveStatus_Success, + ob.chain, + nonce, + coin.CoinType_Gas, + ) + if err != nil { + logger.Error().Err(err).Msgf("error posting confirmation to meta core for cctx %s nonce %d", sendHash, nonce) + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } else if receipt.Status == 0 { // the same as below events flow + logger.Info().Msgf("Found (failed tx) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), receipt.TxHash.Hex()) + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendHash, + receipt.TxHash.Hex(), + receipt.BlockNumber.Uint64(), + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + big.NewInt(0), + chains.ReceiveStatus_Failed, + ob.chain, + nonce, + coin.CoinType_Gas, + ) + if err != nil { + logger.Error().Err(err).Msgf("PostVoteOutbound error in WatchTxHashWithTimeout; zeta tx hash %s cctx %s nonce %d", zetaTxHash, sendHash, nonce) + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } + } else if cointype == coin.CoinType_Zeta { // the outbound is a Zeta transfer; need to check events ZetaReceived + if receipt.Status == 1 { + logs := receipt.Logs + for _, vLog := range logs { + confHeight := vLog.BlockNumber + params.ConfirmationCount + // TODO rewrite this to return early if not confirmed + connectorAddr, connector, err := ob.GetConnectorContract() + if err != nil { + return false, false, fmt.Errorf("error getting connector contract: %w", err) + } + receivedLog, err := connector.ZetaConnectorNonEthFilterer.ParseZetaReceived(*vLog) + if err == nil { + logger.Info().Msgf("Found (outTx) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), vLog.TxHash.Hex()) + if confHeight <= ob.GetLastBlockHeight() { + logger.Info().Msg("Confirmed! Sending PostConfirmation to zetabridge...") + // sanity check tx event + err = ValidateEvmTxLog(vLog, connectorAddr, transaction.Hash().Hex(), TopicsZetaReceived) + if err != nil { + logger.Error().Err(err).Msgf("CheckEvmTxLog error on ZetaReceived event, chain %d nonce %d txhash %s", ob.chain.ChainId, nonce, transaction.Hash().Hex()) + return false, false, err + } + sendhash := vLog.Topics[3].Hex() + //var rxAddress string = ethcommon.HexToAddress(vLog.Topics[1].Hex()).Hex() + mMint := receivedLog.ZetaValue + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendhash, + vLog.TxHash.Hex(), + vLog.BlockNumber, + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + mMint, + chains.ReceiveStatus_Success, + ob.chain, + nonce, + coin.CoinType_Zeta, + ) + if err != nil { + logger.Error().Err(err).Msgf("error posting confirmation to meta core for cctx %s nonce %d", sendHash, nonce) + continue + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } + logger.Info().Msgf("Included; %d blocks before confirmed! chain %s nonce %d", confHeight-ob.GetLastBlockHeight(), ob.chain.String(), nonce) + return true, false, nil + } + revertedLog, err := connector.ZetaConnectorNonEthFilterer.ParseZetaReverted(*vLog) + if err == nil { + logger.Info().Msgf("Found (revertTx) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), vLog.TxHash.Hex()) + if confHeight <= ob.GetLastBlockHeight() { + logger.Info().Msg("Confirmed! Sending PostConfirmation to zetabridge...") + // sanity check tx event + err = ValidateEvmTxLog(vLog, connectorAddr, transaction.Hash().Hex(), TopicsZetaReverted) + if err != nil { + logger.Error().Err(err).Msgf("CheckEvmTxLog error on ZetaReverted event, chain %d nonce %d txhash %s", ob.chain.ChainId, nonce, transaction.Hash().Hex()) + return false, false, err + } + sendhash := vLog.Topics[2].Hex() + mMint := revertedLog.RemainingZetaValue + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendhash, + vLog.TxHash.Hex(), + vLog.BlockNumber, + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + mMint, + chains.ReceiveStatus_Success, + ob.chain, + nonce, + coin.CoinType_Zeta, + ) + if err != nil { + logger.Err(err).Msgf("error posting confirmation to meta core for cctx %s nonce %d", sendHash, nonce) + continue + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } + logger.Info().Msgf("Included; %d blocks before confirmed! chain %s nonce %d", confHeight-ob.GetLastBlockHeight(), ob.chain.String(), nonce) + return true, false, nil + } + } + } else if receipt.Status == 0 { + //FIXME: check nonce here by getTransaction RPC + logger.Info().Msgf("Found (failed tx) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), receipt.TxHash.Hex()) + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendHash, + receipt.TxHash.Hex(), + receipt.BlockNumber.Uint64(), + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + big.NewInt(0), + chains.ReceiveStatus_Failed, + ob.chain, + nonce, + coin.CoinType_Zeta, + ) + if err != nil { + logger.Error().Err(err).Msgf("error posting confirmation to meta core for cctx %s nonce %d", sendHash, nonce) + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } + } else if cointype == coin.CoinType_ERC20 { + if receipt.Status == 1 { + logs := receipt.Logs + addrCustody, ERC20Custody, err := ob.GetERC20CustodyContract() + if err != nil { + logger.Warn().Msgf("NewERC20Custody err: %s", err) + } + for _, vLog := range logs { + event, err := ERC20Custody.ParseWithdrawn(*vLog) + confHeight := vLog.BlockNumber + params.ConfirmationCount + if err == nil { + logger.Info().Msgf("Found (ERC20Custody.Withdrawn Event) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), vLog.TxHash.Hex()) + // sanity check tx event + err = ValidateEvmTxLog(vLog, addrCustody, transaction.Hash().Hex(), TopicsWithdrawn) + if err != nil { + logger.Error().Err(err).Msgf("CheckEvmTxLog error on Withdrawn event, chain %d nonce %d txhash %s", ob.chain.ChainId, nonce, transaction.Hash().Hex()) + return false, false, err + } + if confHeight <= ob.GetLastBlockHeight() { + logger.Info().Msg("Confirmed! Sending PostConfirmation to zetabridge...") + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendHash, + vLog.TxHash.Hex(), + vLog.BlockNumber, + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + event.Amount, + chains.ReceiveStatus_Success, + ob.chain, + nonce, + coin.CoinType_ERC20, + ) + if err != nil { + logger.Error().Err(err).Msgf("error posting confirmation to meta core for cctx %s nonce %d", sendHash, nonce) + continue + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } + logger.Info().Msgf("Included; %d blocks before confirmed! chain %s nonce %d", confHeight-ob.GetLastBlockHeight(), ob.chain.String(), nonce) + return true, false, nil + } + } + } else { + logger.Info().Msgf("Found (failed tx) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), receipt.TxHash.Hex()) + zetaTxHash, ballot, err := ob.zetaBridge.PostVoteOutbound( + sendHash, + receipt.TxHash.Hex(), + receipt.BlockNumber.Uint64(), + receipt.GasUsed, + transaction.GasPrice(), + transaction.Gas(), + big.NewInt(0), + chains.ReceiveStatus_Failed, + ob.chain, + nonce, + coin.CoinType_ERC20, + ) + if err != nil { + logger.Error().Err(err).Msgf("PostVoteOutbound error in WatchTxHashWithTimeout; zeta tx hash %s", zetaTxHash) + } else if zetaTxHash != "" { + logger.Info().Msgf("Zeta tx hash: %s cctx %s nonce %d ballot %s", zetaTxHash, sendHash, nonce, ballot) + } + return true, true, nil + } + } + + return false, false, nil +} + // WatchOutTx watches evm chain for outgoing txs status func (ob *ChainClient) WatchOutTx() { ticker, err := clienttypes.NewDynamicTicker(fmt.Sprintf("EVM_WatchOutTx_%d", ob.chain.ChainId), ob.GetChainParams().OutTxTicker) @@ -420,77 +737,6 @@ func (ob *ChainClient) IsTxConfirmed(nonce uint64) bool { return ob.outTXConfirmedReceipts[ob.GetTxID(nonce)] != nil && ob.outTXConfirmedTransactions[ob.GetTxID(nonce)] != nil } -// checkConfirmedTx checks if a txHash is confirmed -// returns (receipt, transaction, true) if confirmed or (nil, nil, false) otherwise -func (ob *ChainClient) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction, bool) { - ctxt, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - // query transaction - transaction, isPending, err := ob.evmClient.TransactionByHash(ctxt, ethcommon.HexToHash(txHash)) - if err != nil { - log.Error().Err(err).Msgf("confirmTxByHash: error getting transaction for outtx %s chain %d", txHash, ob.chain.ChainId) - return nil, nil, false - } - if transaction == nil { // should not happen - log.Error().Msgf("confirmTxByHash: transaction is nil for txHash %s nonce %d", txHash, nonce) - return nil, nil, false - } - - // check tx sender and nonce - 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 outtx %s chain %d", transaction.Hash().Hex(), ob.chain.ChainId) - return nil, nil, false - } - if from != ob.Tss.EVMAddress() { // must be TSS address - log.Error().Msgf("confirmTxByHash: sender %s for outtx %s chain %d is not TSS address %s", - from.Hex(), transaction.Hash().Hex(), ob.chain.ChainId, ob.Tss.EVMAddress().Hex()) - return nil, nil, false - } - if transaction.Nonce() != nonce { // must match cctx nonce - log.Error().Msgf("confirmTxByHash: outtx %s nonce mismatch: wanted %d, got tx nonce %d", txHash, nonce, transaction.Nonce()) - return nil, nil, false - } - - // save pending transaction - if isPending { - ob.SetPendingTx(nonce, transaction) - return nil, nil, false - } - - // query receipt - receipt, err := ob.evmClient.TransactionReceipt(ctxt, ethcommon.HexToHash(txHash)) - if err != nil { - if err != ethereum.NotFound { - log.Warn().Err(err).Msgf("confirmTxByHash: TransactionReceipt error, txHash %s nonce %d", txHash, nonce) - } - return nil, nil, false - } - if receipt == nil { // should not happen - log.Error().Msgf("confirmTxByHash: receipt is nil for txHash %s nonce %d", txHash, nonce) - return nil, nil, false - } - - // check confirmations - if !ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()) { - log.Debug().Msgf("confirmTxByHash: txHash %s nonce %d included but not confirmed: receipt block %d, current block %d", - txHash, nonce, receipt.BlockNumber, ob.GetLastBlockHeight()) - return nil, nil, false - } - - // cross-check tx inclusion against the block - // Note: a guard for false BlockNumber in receipt. The blob-carrying tx won't come here - err = ob.CheckTxInclusion(transaction, receipt) - if err != nil { - log.Error().Err(err).Msgf("confirmTxByHash: checkTxInclusion error for txHash %s nonce %d", txHash, nonce) - return nil, nil, false - } - - return receipt, transaction, true -} - // CheckTxInclusion returns nil only if tx is included at the position indicated by the receipt ([block, index]) func (ob *ChainClient) CheckTxInclusion(tx *ethtypes.Transaction, receipt *ethtypes.Receipt) error { block, err := ob.GetBlockByNumberCached(receipt.BlockNumber.Uint64()) @@ -498,17 +744,20 @@ func (ob *ChainClient) CheckTxInclusion(tx *ethtypes.Transaction, receipt *ethty return errors.Wrapf(err, "GetBlockByNumberCached error for block %d txHash %s nonce %d", receipt.BlockNumber.Uint64(), tx.Hash(), tx.Nonce()) } + // #nosec G701 non negative value if receipt.TransactionIndex >= uint(len(block.Transactions)) { return fmt.Errorf("transaction index %d out of range [0, %d), txHash %s nonce %d block %d", receipt.TransactionIndex, len(block.Transactions), tx.Hash(), tx.Nonce(), receipt.BlockNumber.Uint64()) } + txAtIndex := block.Transactions[receipt.TransactionIndex] if !strings.EqualFold(txAtIndex.Hash, tx.Hash().Hex()) { ob.RemoveCachedBlock(receipt.BlockNumber.Uint64()) // clean stale block from cache return fmt.Errorf("transaction at index %d has different hash %s, txHash %s nonce %d block %d", receipt.TransactionIndex, txAtIndex.Hash, tx.Hash(), tx.Nonce(), receipt.BlockNumber.Uint64()) } + return nil } @@ -548,10 +797,11 @@ func (ob *ChainClient) WatchInTx() { ob.logger.InTx.Error().Err(err).Msg("error creating ticker") return } - defer ticker.Stop() + ob.logger.InTx.Info().Msgf("WatchInTx started for chain %d", ob.chain.ChainId) sampledLogger := ob.logger.InTx.Sample(&zerolog.BasicSampler{N: 10}) + for { select { case <-ticker.C(): @@ -571,114 +821,6 @@ func (ob *ChainClient) WatchInTx() { } } -// calcBlockRangeToScan calculates the next range of blocks to scan -func (ob *ChainClient) calcBlockRangeToScan(latestConfirmed, lastScanned, batchSize uint64) (uint64, uint64) { - startBlock := lastScanned + 1 - toBlock := lastScanned + batchSize - if toBlock > latestConfirmed { - toBlock = latestConfirmed - } - return startBlock, toBlock -} - -func (ob *ChainClient) postBlockHeader(tip uint64) error { - bn := tip - - res, err := ob.zetaBridge.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 - } - - if bn > tip { - return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) - } - - header, err := ob.GetBlockHeaderCached(bn) - if err != nil { - ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) - return err - } - headerRLP, err := rlp.EncodeToBytes(header) - if err != nil { - ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) - return err - } - - _, err = ob.zetaBridge.PostVoteBlockHeader( - ob.chain.ChainId, - header.Hash().Bytes(), - header.Number.Int64(), - proofs.NewEthereumHeader(headerRLP), - ) - if err != nil { - ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) - return err - } - return nil -} - -func (ob *ChainClient) observeInTX(sampledLogger zerolog.Logger) error { - // get and update latest block height - blockNumber, err := ob.evmClient.BlockNumber(context.Background()) - if err != nil { - return err - } - if blockNumber < ob.GetLastBlockHeight() { - return fmt.Errorf("observeInTX: block number should not decrease: current %d last %d", blockNumber, ob.GetLastBlockHeight()) - } - ob.SetLastBlockHeight(blockNumber) - - // increment prom counter - metrics.GetBlockByNumberPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() - - // skip if current height is too low - if blockNumber < ob.GetChainParams().ConfirmationCount { - return fmt.Errorf("observeInTX: skipping observer, current block number %d is too low", blockNumber) - } - confirmedBlockNum := blockNumber - ob.GetChainParams().ConfirmationCount - - // skip if no new block is confirmed - lastScanned := ob.GetLastBlockHeightScanned() - if lastScanned >= confirmedBlockNum { - sampledLogger.Debug().Msgf("observeInTX: skipping observer, no new block is produced for chain %d", ob.chain.ChainId) - return nil - } - - // get last scanned block height (we simply use same height for all 3 events ZetaSent, Deposited, TssRecvd) - // Note: using different heights for each event incurs more complexity (metrics, db, etc) and not worth it - startBlock, toBlock := ob.calcBlockRangeToScan(confirmedBlockNum, lastScanned, config.MaxBlocksPerPeriod) - - // task 1: query evm chain for zeta sent logs (read at most 100 blocks in one go) - lastScannedZetaSent := ob.ObserveZetaSent(startBlock, toBlock) - - // task 2: query evm chain for deposited logs (read at most 100 blocks in one go) - lastScannedDeposited := ob.ObserveERC20Deposited(startBlock, toBlock) - - // task 3: query the incoming tx to TSS address (read at most 100 blocks in one go) - lastScannedTssRecvd := ob.ObserverTSSReceive(startBlock, toBlock) - - // note: using lowest height for all 3 events is not perfect, but it's simple and good enough - lastScannedLowest := lastScannedZetaSent - if lastScannedDeposited < lastScannedLowest { - lastScannedLowest = lastScannedDeposited - } - if lastScannedTssRecvd < lastScannedLowest { - lastScannedLowest = lastScannedTssRecvd - } - - // update last scanned block height for all 3 events (ZetaSent, Deposited, TssRecvd), ignore db error - if lastScannedLowest > lastScanned { - sampledLogger.Info().Msgf("observeInTX: 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.InTx.Error().Err(err).Msgf("observeInTX: error writing lastScannedLowest %d to db", lastScannedLowest) - } - } - return nil -} - // ObserveZetaSent queries the ZetaSent event from the connector contract and posts to zetabridge // returns the last block successfully scanned func (ob *ChainClient) ObserveZetaSent(startBlock, toBlock uint64) uint64 { @@ -760,6 +902,7 @@ func (ob *ChainClient) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 ob.logger.InTx.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") return startBlock - 1 // lastScanned } + iter, err := erc20custodyContract.FilterDeposited(&bind.FilterOpts{ Start: startBlock, End: &toBlock, @@ -960,24 +1103,6 @@ func (ob *ChainClient) BuildLastBlock() error { return nil } -func (ob *ChainClient) BuildReceiptsMap() error { - logger := ob.logger - var receipts []clienttypes.ReceiptSQLType - if err := ob.db.Find(&receipts).Error; err != nil { - logger.Chain.Error().Err(err).Msg("error iterating over db") - return err - } - for _, receipt := range receipts { - r, err := clienttypes.FromReceiptDBType(receipt.Receipt) - if err != nil { - return err - } - ob.outTXConfirmedReceipts[receipt.Identifier] = r - } - - return nil -} - // LoadDB open sql database and load data into EVMChainClient func (ob *ChainClient) LoadDB(dbPath string, chain chains.Chain) error { if dbPath != "" { @@ -1077,3 +1202,182 @@ func (ob *ChainClient) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block func (ob *ChainClient) RemoveCachedBlock(blockNumber uint64) { ob.blockCache.Remove(blockNumber) } + +// checkConfirmedTx checks if a txHash is confirmed +// returns (receipt, transaction, true) if confirmed or (nil, nil, false) otherwise +func (ob *ChainClient) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction, bool) { + ctxt, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + // query transaction + transaction, isPending, err := ob.evmClient.TransactionByHash(ctxt, ethcommon.HexToHash(txHash)) + if err != nil { + log.Error().Err(err).Msgf("confirmTxByHash: error getting transaction for outtx %s chain %d", txHash, ob.chain.ChainId) + return nil, nil, false + } + if transaction == nil { // should not happen + log.Error().Msgf("confirmTxByHash: transaction is nil for txHash %s nonce %d", txHash, nonce) + return nil, nil, false + } + + // check tx sender and nonce + 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 outtx %s chain %d", transaction.Hash().Hex(), ob.chain.ChainId) + return nil, nil, false + } + if from != ob.Tss.EVMAddress() { // must be TSS address + log.Error().Msgf("confirmTxByHash: sender %s for outtx %s chain %d is not TSS address %s", + from.Hex(), transaction.Hash().Hex(), ob.chain.ChainId, ob.Tss.EVMAddress().Hex()) + return nil, nil, false + } + if transaction.Nonce() != nonce { // must match cctx nonce + log.Error().Msgf("confirmTxByHash: outtx %s nonce mismatch: wanted %d, got tx nonce %d", txHash, nonce, transaction.Nonce()) + return nil, nil, false + } + + // save pending transaction + if isPending { + ob.SetPendingTx(nonce, transaction) + return nil, nil, false + } + + // query receipt + receipt, err := ob.evmClient.TransactionReceipt(ctxt, ethcommon.HexToHash(txHash)) + if err != nil { + if err != ethereum.NotFound { + log.Warn().Err(err).Msgf("confirmTxByHash: TransactionReceipt error, txHash %s nonce %d", txHash, nonce) + } + return nil, nil, false + } + if receipt == nil { // should not happen + log.Error().Msgf("confirmTxByHash: receipt is nil for txHash %s nonce %d", txHash, nonce) + return nil, nil, false + } + + // check confirmations + if !ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()) { + log.Debug().Msgf("confirmTxByHash: txHash %s nonce %d included but not confirmed: receipt block %d, current block %d", + txHash, nonce, receipt.BlockNumber, ob.GetLastBlockHeight()) + return nil, nil, false + } + + // cross-check tx inclusion against the block + // Note: a guard for false BlockNumber in receipt. The blob-carrying tx won't come here + err = ob.CheckTxInclusion(transaction, receipt) + if err != nil { + log.Error().Err(err).Msgf("confirmTxByHash: checkTxInclusion error for txHash %s nonce %d", txHash, nonce) + return nil, nil, false + } + + return receipt, transaction, true +} + +// calcBlockRangeToScan calculates the next range of blocks to scan +func (ob *ChainClient) calcBlockRangeToScan(latestConfirmed, lastScanned, batchSize uint64) (uint64, uint64) { + startBlock := lastScanned + 1 + toBlock := lastScanned + batchSize + if toBlock > latestConfirmed { + toBlock = latestConfirmed + } + return startBlock, toBlock +} + +func (ob *ChainClient) postBlockHeader(tip uint64) error { + bn := tip + + res, err := ob.zetaBridge.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 + } + + if bn > tip { + return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) + } + + header, err := ob.GetBlockHeaderCached(bn) + if err != nil { + ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) + return err + } + headerRLP, err := rlp.EncodeToBytes(header) + if err != nil { + ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) + return err + } + + _, err = ob.zetaBridge.PostVoteBlockHeader( + ob.chain.ChainId, + header.Hash().Bytes(), + header.Number.Int64(), + proofs.NewEthereumHeader(headerRLP), + ) + if err != nil { + ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) + return err + } + return nil +} + +func (ob *ChainClient) observeInTX(sampledLogger zerolog.Logger) error { + // get and update latest block height + blockNumber, err := ob.evmClient.BlockNumber(context.Background()) + if err != nil { + return err + } + if blockNumber < ob.GetLastBlockHeight() { + return fmt.Errorf("observeInTX: block number should not decrease: current %d last %d", blockNumber, ob.GetLastBlockHeight()) + } + ob.SetLastBlockHeight(blockNumber) + + // increment prom counter + metrics.GetBlockByNumberPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + + // skip if current height is too low + if blockNumber < ob.GetChainParams().ConfirmationCount { + return fmt.Errorf("observeInTX: skipping observer, current block number %d is too low", blockNumber) + } + confirmedBlockNum := blockNumber - ob.GetChainParams().ConfirmationCount + + // skip if no new block is confirmed + lastScanned := ob.GetLastBlockHeightScanned() + if lastScanned >= confirmedBlockNum { + sampledLogger.Debug().Msgf("observeInTX: skipping observer, no new block is produced for chain %d", ob.chain.ChainId) + return nil + } + + // get last scanned block height (we simply use same height for all 3 events ZetaSent, Deposited, TssRecvd) + // Note: using different heights for each event incurs more complexity (metrics, db, etc) and not worth it + startBlock, toBlock := ob.calcBlockRangeToScan(confirmedBlockNum, lastScanned, config.MaxBlocksPerPeriod) + + // task 1: query evm chain for zeta sent logs (read at most 100 blocks in one go) + lastScannedZetaSent := ob.ObserveZetaSent(startBlock, toBlock) + + // task 2: query evm chain for deposited logs (read at most 100 blocks in one go) + lastScannedDeposited := ob.ObserveERC20Deposited(startBlock, toBlock) + + // task 3: query the incoming tx to TSS address (read at most 100 blocks in one go) + lastScannedTssRecvd := ob.ObserverTSSReceive(startBlock, toBlock) + + // note: using lowest height for all 3 events is not perfect, but it's simple and good enough + lastScannedLowest := lastScannedZetaSent + if lastScannedDeposited < lastScannedLowest { + lastScannedLowest = lastScannedDeposited + } + if lastScannedTssRecvd < lastScannedLowest { + lastScannedLowest = lastScannedTssRecvd + } + + // update last scanned block height for all 3 events (ZetaSent, Deposited, TssRecvd), ignore db error + if lastScannedLowest > lastScanned { + sampledLogger.Info().Msgf("observeInTX: 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.InTx.Error().Err(err).Msgf("observeInTX: error writing lastScannedLowest %d to db", lastScannedLowest) + } + } + return nil +} diff --git a/zetaclient/evm/evm_signer.go b/zetaclient/evm/evm_signer.go index d9b6eb25dd..9c2b0e1716 100644 --- a/zetaclient/evm/evm_signer.go +++ b/zetaclient/evm/evm_signer.go @@ -36,6 +36,8 @@ import ( zbridge "github.com/zeta-chain/zetacore/zetaclient/zetabridge" ) +var _ interfaces.ChainSigner = &Signer{} + // Signer deals with the signing EVM transactions and implements the ChainSigner interface type Signer struct { client interfaces.EVMRPCClient @@ -55,8 +57,6 @@ type Signer struct { outTxHashBeingReported map[string]bool } -var _ interfaces.ChainSigner = &Signer{} - func NewEVMSigner( chain chains.Chain, endpoint string, @@ -152,17 +152,20 @@ func (signer *Signer) Sign( if err != nil { return nil, nil, nil, err } + log.Debug().Msgf("Sign: Signature: %s", hex.EncodeToString(sig[:])) pubk, err := crypto.SigToPub(hashBytes, sig[:]) if err != nil { signer.logger.Std.Error().Err(err).Msgf("SigToPub error") } + addr := crypto.PubkeyToAddress(*pubk) signer.logger.Std.Info().Msgf("Sign: Ecrecovery of signature: %s", addr.Hex()) signedTX, err := tx.WithSignature(signer.ethSigner, sig[:]) if err != nil { return nil, nil, nil, err } + return signedTX, sig[:], hashBytes[:], nil } @@ -262,10 +265,12 @@ func (signer *Signer) SignCancelTx(nonce uint64, gasPrice *big.Int, height uint6 if err != nil { return nil, err } + pubk, err := crypto.SigToPub(hashBytes, sig[:]) if err != nil { signer.logger.Std.Error().Err(err).Msgf("SigToPub error") } + addr := crypto.PubkeyToAddress(*pubk) signer.logger.Std.Info().Msgf("Sign: Ecrecovery of signature: %s", addr.Hex()) signedTX, err := tx.WithSignature(signer.ethSigner, sig[:]) @@ -287,10 +292,12 @@ func (signer *Signer) SignWithdrawTx(txData *OutBoundTransactionData) (*ethtypes if err != nil { return nil, err } + pubk, err := crypto.SigToPub(hashBytes, sig[:]) if err != nil { signer.logger.Std.Error().Err(err).Msgf("SigToPub error") } + addr := crypto.PubkeyToAddress(*pubk) signer.logger.Std.Info().Msgf("Sign: Ecrecovery of signature: %s", addr.Hex()) signedTX, err := tx.WithSignature(signer.ethSigner, sig[:]) @@ -495,6 +502,116 @@ func (signer *Signer) BroadcastOutTx( } } +// SignERC20WithdrawTx +// function withdraw( +// address recipient, +// address asset, +// uint256 amount, +// ) external onlyTssAddress +func (signer *Signer) SignERC20WithdrawTx(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { + var data []byte + var err error + data, err = signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) + if err != nil { + return nil, fmt.Errorf("pack error: %w", err) + } + + tx, _, _, err := signer.Sign(data, signer.er20CustodyAddress, txData.gasLimit, txData.gasPrice, txData.nonce, txData.height) + if err != nil { + return nil, fmt.Errorf("sign error: %w", err) + } + + return tx, nil +} + +// SignWhitelistTx +// function whitelist( +// address asset, +// ) external onlyTssAddress +// function unwhitelist( +// address asset, +// ) external onlyTssAddress +func (signer *Signer) SignWhitelistTx( + action string, + _ ethcommon.Address, + asset ethcommon.Address, + gasLimit uint64, + nonce uint64, + gasPrice *big.Int, + height uint64, +) (*ethtypes.Transaction, error) { + var data []byte + + var err error + + data, err = signer.erc20CustodyABI.Pack(action, asset) + if err != nil { + return nil, fmt.Errorf("pack error: %w", err) + } + + tx, _, _, err := signer.Sign(data, signer.er20CustodyAddress, gasLimit, gasPrice, nonce, height) + if err != nil { + return nil, fmt.Errorf("Sign error: %w", err) + } + + return tx, nil +} + +// Exported for unit tests + +// GetReportedTxList returns a list of outTxHash being reported +// TODO: investigate pointer usage +// https://github.com/zeta-chain/node/issues/2084 +func (signer *Signer) GetReportedTxList() *map[string]bool { + return &signer.outTxHashBeingReported +} + +func (signer *Signer) EvmClient() interfaces.EVMRPCClient { + return signer.client +} + +func (signer *Signer) EvmSigner() ethtypes.Signer { + return signer.ethSigner +} + +func IsSenderZetaChain(cctx *types.CrossChainTx, zetaBridge interfaces.ZetaCoreBridger, flags *observertypes.CrosschainFlags) bool { + return cctx.InboundTxParams.SenderChainId == zetaBridge.ZetaChain().ChainId && cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound && flags.IsOutboundEnabled +} + +func SignerErrorMsg(cctx *types.CrossChainTx) string { + return fmt.Sprintf("signer SignOutbound error: nonce %d chain %d", cctx.GetCurrentOutTxParam().OutboundTxTssNonce, cctx.GetCurrentOutTxParam().ReceiverChainId) +} + +func (signer *Signer) SignWhitelistERC20Cmd(txData *OutBoundTransactionData, params string) (*ethtypes.Transaction, error) { + outboundParams := txData.outboundParams + erc20 := ethcommon.HexToAddress(params) + if erc20 == (ethcommon.Address{}) { + return nil, fmt.Errorf("SignCommandTx: invalid erc20 address %s", params) + } + custodyAbi, err := erc20custody.ERC20CustodyMetaData.GetAbi() + if err != nil { + return nil, err + } + data, err := custodyAbi.Pack("whitelist", erc20) + if err != nil { + return nil, err + } + tx, _, _, err := signer.Sign(data, txData.to, txData.gasLimit, txData.gasPrice, outboundParams.OutboundTxTssNonce, txData.height) + if err != nil { + return nil, fmt.Errorf("sign error: %w", err) + } + return tx, nil +} + +func (signer *Signer) SignMigrateTssFundsCmd(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { + outboundParams := txData.outboundParams + tx, _, _, err := signer.Sign(nil, txData.to, txData.gasLimit, txData.gasPrice, outboundParams.OutboundTxTssNonce, txData.height) + if err != nil { + return nil, err + } + return tx, nil +} + // reportToOutTxTracker reports outTxHash to tracker only when tx receipt is available func (signer *Signer) reportToOutTxTracker(zetaBridge interfaces.ZetaCoreBridger, chainID int64, nonce uint64, outTxHash string, logger zerolog.Logger) { // skip if already being reported @@ -590,75 +707,6 @@ func (signer *Signer) reportToOutTxTracker(zetaBridge interfaces.ZetaCoreBridger }() } -// SignERC20WithdrawTx -// function withdraw( -// address recipient, -// address asset, -// uint256 amount, -// ) external onlyTssAddress -func (signer *Signer) SignERC20WithdrawTx(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { - var data []byte - var err error - data, err = signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) - if err != nil { - return nil, fmt.Errorf("pack error: %w", err) - } - - tx, _, _, err := signer.Sign(data, signer.er20CustodyAddress, txData.gasLimit, txData.gasPrice, txData.nonce, txData.height) - if err != nil { - return nil, fmt.Errorf("sign error: %w", err) - } - - return tx, nil -} - -// SignWhitelistTx -// function whitelist( -// address asset, -// ) external onlyTssAddress -// function unwhitelist( -// address asset, -// ) external onlyTssAddress -func (signer *Signer) SignWhitelistTx( - action string, - _ ethcommon.Address, - asset ethcommon.Address, - gasLimit uint64, - nonce uint64, - gasPrice *big.Int, - height uint64, -) (*ethtypes.Transaction, error) { - var data []byte - - var err error - - data, err = signer.erc20CustodyABI.Pack(action, asset) - if err != nil { - return nil, fmt.Errorf("pack error: %w", err) - } - - tx, _, _, err := signer.Sign(data, signer.er20CustodyAddress, gasLimit, gasPrice, nonce, height) - if err != nil { - return nil, fmt.Errorf("Sign error: %w", err) - } - - return tx, nil -} - -// Exported for unit tests - -func (signer *Signer) GetReportedTxList() *map[string]bool { - return &signer.outTxHashBeingReported -} -func (signer *Signer) EvmClient() interfaces.EVMRPCClient { - return signer.client -} -func (signer *Signer) EvmSigner() ethtypes.Signer { - return signer.ethSigner -} - -// ________________________ - // getEVMRPC is a helper function to set up the client and signer, also initializes a mock client for unit tests func getEVMRPC(endpoint string) (interfaces.EVMRPCClient, ethtypes.Signer, error) { if endpoint == stub.EVMRPCEnabled { @@ -690,41 +738,3 @@ func roundUpToNearestGwei(gasPrice *big.Int) *big.Int { } return new(big.Int).Add(gasPrice, new(big.Int).Sub(oneGwei, mod)) } - -func IsSenderZetaChain(cctx *types.CrossChainTx, zetaBridge interfaces.ZetaCoreBridger, flags *observertypes.CrosschainFlags) bool { - return cctx.InboundTxParams.SenderChainId == zetaBridge.ZetaChain().ChainId && cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound && flags.IsOutboundEnabled -} - -func SignerErrorMsg(cctx *types.CrossChainTx) string { - return fmt.Sprintf("signer SignOutbound error: nonce %d chain %d", cctx.GetCurrentOutTxParam().OutboundTxTssNonce, cctx.GetCurrentOutTxParam().ReceiverChainId) -} - -func (signer *Signer) SignWhitelistERC20Cmd(txData *OutBoundTransactionData, params string) (*ethtypes.Transaction, error) { - outboundParams := txData.outboundParams - erc20 := ethcommon.HexToAddress(params) - if erc20 == (ethcommon.Address{}) { - return nil, fmt.Errorf("SignCommandTx: invalid erc20 address %s", params) - } - custodyAbi, err := erc20custody.ERC20CustodyMetaData.GetAbi() - if err != nil { - return nil, err - } - data, err := custodyAbi.Pack("whitelist", erc20) - if err != nil { - return nil, err - } - tx, _, _, err := signer.Sign(data, txData.to, txData.gasLimit, txData.gasPrice, outboundParams.OutboundTxTssNonce, txData.height) - if err != nil { - return nil, fmt.Errorf("sign error: %w", err) - } - return tx, nil -} - -func (signer *Signer) SignMigrateTssFundsCmd(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { - outboundParams := txData.outboundParams - tx, _, _, err := signer.Sign(nil, txData.to, txData.gasLimit, txData.gasPrice, outboundParams.OutboundTxTssNonce, txData.height) - if err != nil { - return nil, err - } - return tx, nil -} diff --git a/zetaclient/evm/inbounds.go b/zetaclient/evm/inbounds.go index 0ee0e0457f..deb94d38a7 100644 --- a/zetaclient/evm/inbounds.go +++ b/zetaclient/evm/inbounds.go @@ -37,8 +37,8 @@ func (ob *ChainClient) WatchIntxTracker() { ob.logger.InTx.Err(err).Msg("error creating ticker") return } - defer ticker.Stop() + ob.logger.InTx.Info().Msgf("Intx tracker watcher started for chain %d", ob.chain.ChainId) for { select { @@ -70,6 +70,7 @@ func (ob *ChainClient) ObserveIntxTrackers() error { if err != nil { return errors.Wrapf(err, "error getting transaction for intx %s chain %d", tracker.TxHash, 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 intx %s chain %d", tracker.TxHash, ob.chain.ChainId) @@ -100,6 +101,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenZeta(tx *ethrpc.Transaction, rece if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { return "", fmt.Errorf("intx %s has not been confirmed yet: receipt block %d", tx.Hash, receipt.BlockNumber.Uint64()) } + // get zeta connector contract addrConnector, connector, err := ob.GetConnectorContract() if err != nil { @@ -130,6 +132,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenZeta(tx *ethrpc.Transaction, rece if vote { return ob.PostVoteInbound(msg, coin.CoinType_Zeta, zetabridge.PostVoteInboundMessagePassingExecutionGasLimit) } + return msg.Digest(), nil } @@ -171,6 +174,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenERC20(tx *ethrpc.Transaction, rec if vote { return ob.PostVoteInbound(msg, coin.CoinType_ERC20, zetabridge.PostVoteInboundExecutionGasLimit) } + return msg.Digest(), nil } @@ -180,6 +184,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenGas(tx *ethrpc.Transaction, recei if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { return "", fmt.Errorf("intx %s has not been confirmed yet: receipt block %d", tx.Hash, receipt.BlockNumber.Uint64()) } + // checks receiver and tx status if ethcommon.HexToAddress(tx.To) != ob.Tss.EVMAddress() { return "", fmt.Errorf("tx.To %s is not TSS address", tx.To) @@ -199,6 +204,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenGas(tx *ethrpc.Transaction, recei if vote { return ob.PostVoteInbound(msg, coin.CoinType_Gas, zetabridge.PostVoteInboundExecutionGasLimit) } + return msg.Digest(), nil } @@ -215,6 +221,7 @@ func (ob *ChainClient) PostVoteInbound(msg *types.MsgVoteOnObservedInboundTx, co } else { ob.logger.InTx.Info().Msgf("intx detected: chain %d token %s intx %s already voted on ballot %s", chainID, coinType, txHash, ballot) } + return ballot, err } @@ -366,6 +373,7 @@ func (ob *ChainClient) ObserveTSSReceiveInBlock(blockNumber uint64) error { if err != nil { 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() { @@ -373,6 +381,7 @@ func (ob *ChainClient) ObserveTSSReceiveInBlock(blockNumber uint64) error { if err != nil { return errors.Wrapf(err, "error getting receipt for intx %s chain %d", tx.Hash, ob.chain.ChainId) } + _, err = ob.CheckAndVoteInboundTokenGas(&tx, receipt, true) if err != nil { return errors.Wrapf(err, "error checking and voting inbound gas asset for intx %s chain %d", tx.Hash, ob.chain.ChainId) diff --git a/zetaclient/interfaces/interfaces.go b/zetaclient/interfaces/interfaces.go index 0fbaba26b3..91695c063b 100644 --- a/zetaclient/interfaces/interfaces.go +++ b/zetaclient/interfaces/interfaces.go @@ -94,7 +94,6 @@ type ZetaCoreBridger interface { txIndex int64, ) (string, error) GetKeys() *keys.Keys - GetBlockHeight() (int64, error) GetZetaBlockHeight() (int64, error) GetLastBlockHeightByChain(chain chains.Chain) (*crosschaintypes.LastBlockHeight, error) ListPendingCctx(chainID int64) ([]*crosschaintypes.CrossChainTx, uint64, error) diff --git a/zetaclient/interfaces/signer.go b/zetaclient/interfaces/signer.go index 461902359c..11142f8087 100644 --- a/zetaclient/interfaces/signer.go +++ b/zetaclient/interfaces/signer.go @@ -16,8 +16,13 @@ import ( type TSSSigner interface { Pubkey() []byte - // Sign: Specify optionalPubkey to use a different pubkey than the current pubkey set during keygen + + // Sign signs the data + // Note: it specifies optionalPubkey to use a different pubkey than the current pubkey set during keygen + // TODO: check if optionalPubkey is needed + // https://github.com/zeta-chain/node/issues/2085 Sign(data []byte, height uint64, nonce uint64, chain *chains.Chain, optionalPubkey string) ([65]byte, error) + EVMAddress() ethcommon.Address BTCAddress() string BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash @@ -90,6 +95,7 @@ func (s TestSigner) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyH fmt.Printf("error parsing pubkey: %v", err) return nil } + // witness program: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#Witness_program // The HASH160 of the public key must match the 20-byte witness program. addrWPKH, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pk.SerializeCompressed()), &chaincfg.TestNet3Params) diff --git a/zetaclient/keys/keys.go b/zetaclient/keys/keys.go index 1ca248aff9..4c3a4c48c1 100644 --- a/zetaclient/keys/keys.go +++ b/zetaclient/keys/keys.go @@ -14,7 +14,6 @@ import ( cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/rs/zerolog/log" - "github.com/zeta-chain/zetacore/cmd" "github.com/zeta-chain/zetacore/pkg/cosmos" zetacrypto "github.com/zeta-chain/zetacore/pkg/crypto" "github.com/zeta-chain/zetacore/zetaclient/config" @@ -86,25 +85,6 @@ func GetKeyringKeybase(cfg config.Config, hotkeyPassword string) (ckeys.Keyring, return kb, pubkeyBech32, nil } -// getKeybase will create an instance of Keybase -func getKeybase(zetaCoreHome string, reader io.Reader, keyringBackend config.KeyringBackend) (ckeys.Keyring, error) { - cliDir := zetaCoreHome - if len(zetaCoreHome) == 0 { - return nil, fmt.Errorf("zetaCoreHome is empty") - } - registry := codectypes.NewInterfaceRegistry() - cryptocodec.RegisterInterfaces(registry) - cdc := codec.NewProtoCodec(registry) - - // create a new keybase based on the selected backend - backend := ckeys.BackendTest - if keyringBackend == config.KeyringBackendFile { - backend = ckeys.BackendFile - } - - return ckeys.New(sdk.KeyringServiceName(), backend, cliDir, reader, cdc) -} - // GetSignerInfo return signer info func (k *Keys) GetSignerInfo() *ckeys.Record { signer := GetGranteeKeyName(k.signerName) @@ -183,14 +163,21 @@ func (k *Keys) GetHotkeyPassword() string { return "" } -func SetupConfigForTest() { - config := sdk.GetConfig() - config.SetBech32PrefixForAccount(cmd.Bech32PrefixAccAddr, cmd.Bech32PrefixAccPub) - config.SetBech32PrefixForValidator(cmd.Bech32PrefixValAddr, cmd.Bech32PrefixValPub) - config.SetBech32PrefixForConsensusNode(cmd.Bech32PrefixConsAddr, cmd.Bech32PrefixConsPub) - //config.SetCoinType(cmd.MetaChainCoinType) - config.SetFullFundraiserPath(cmd.ZetaChainHDPath) - sdk.SetCoinDenomRegex(func() string { - return cmd.DenomRegex - }) +// getKeybase will create an instance of Keybase +func getKeybase(zetaCoreHome string, reader io.Reader, keyringBackend config.KeyringBackend) (ckeys.Keyring, error) { + cliDir := zetaCoreHome + if len(zetaCoreHome) == 0 { + return nil, fmt.Errorf("zetaCoreHome is empty") + } + registry := codectypes.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(registry) + cdc := codec.NewProtoCodec(registry) + + // create a new keybase based on the selected backend + backend := ckeys.BackendTest + if keyringBackend == config.KeyringBackendFile { + backend = ckeys.BackendFile + } + + return ckeys.New(sdk.KeyringServiceName(), backend, cliDir, reader, cdc) } diff --git a/zetaclient/keys/keys_test.go b/zetaclient/keys/keys_test.go index 347195869f..1d81397531 100644 --- a/zetaclient/keys/keys_test.go +++ b/zetaclient/keys/keys_test.go @@ -28,10 +28,6 @@ func Test(t *testing.T) { TestingT(t) } var _ = Suite(&KeysSuite{}) -func (*KeysSuite) SetUpSuite(c *C) { - SetupConfigForTest() -} - var ( password = "password" ) @@ -41,6 +37,21 @@ const ( signerPasswordForTest = `password` ) +func setupConfig() { + testConfig := sdk.GetConfig() + testConfig.SetBech32PrefixForAccount(cmd.Bech32PrefixAccAddr, cmd.Bech32PrefixAccPub) + testConfig.SetBech32PrefixForValidator(cmd.Bech32PrefixValAddr, cmd.Bech32PrefixValPub) + testConfig.SetBech32PrefixForConsensusNode(cmd.Bech32PrefixConsAddr, cmd.Bech32PrefixConsPub) + testConfig.SetFullFundraiserPath(cmd.ZetaChainHDPath) + sdk.SetCoinDenomRegex(func() string { + return cmd.DenomRegex + }) +} + +func (*KeysSuite) SetUpSuite(_ *C) { + setupConfig() +} + func (*KeysSuite) setupKeysForTest(c *C) string { ns := strconv.Itoa(time.Now().Nanosecond()) metaCliDir := filepath.Join(os.TempDir(), ns, ".metacli") diff --git a/zetaclient/metrics/burn_rate.go b/zetaclient/metrics/burn_rate.go index e4c6053ccd..844a862fb7 100644 --- a/zetaclient/metrics/burn_rate.go +++ b/zetaclient/metrics/burn_rate.go @@ -6,6 +6,7 @@ import ( sdkmath "cosmossdk.io/math" ) +// BurnRate calculates the average burn rate for a range of blocks. type BurnRate struct { blockLow int64 blockHigh int64 @@ -14,6 +15,7 @@ type BurnRate struct { queue []int64 } +// NewBurnRate creates a new BurnRate instance with a window size. func NewBurnRate(windowSize int64) *BurnRate { return &BurnRate{ blockLow: 1, @@ -24,9 +26,8 @@ func NewBurnRate(windowSize int64) *BurnRate { } } -// AddFee - adds fee amount spent on a tx for a particular block. It is added to a queue which is used to calculate -// -// the average burn rate for a range of blocks determined by the window size. +// AddFee adds fee amount spent on a tx for a particular block. It is added to a queue which is used to calculate +// the average burn rate for a range of blocks determined by the window size. func (br *BurnRate) AddFee(amount int64, block int64) error { // Check if block is in range of the window if block < br.blockLow { @@ -58,9 +59,8 @@ func (br *BurnRate) AddFee(amount int64, block int64) error { return nil } -// enqueueEntry - add fee entry into queue if is in range of the window. A padding is added if the block height is -// -// more than one block greater than the highest range. +// enqueueEntry adds fee entry into queue if is in range of the window. A padding is added if the block height is +// more than one block greater than the highest range. func (br *BurnRate) enqueueEntry(block int64, amount int64) error { diff := block - br.blockHigh if diff < 1 { @@ -82,7 +82,8 @@ func (br *BurnRate) enqueueEntry(block int64, amount int64) error { return nil } -// dequeueOldEntries - when the window slides forward, older entries in the queue need to be cleared. +// dequeueOldEntries dequeues old entries +// when the window slides forward, older entries in the queue need to be cleared. func (br *BurnRate) dequeueOldEntries() error { diff := br.blockHigh - br.blockLow if diff < br.windowSize { @@ -102,7 +103,7 @@ func (br *BurnRate) dequeueOldEntries() error { return nil } -// GetBurnRate - calculate current burn rate and return the value. +// GetBurnRate calculates current burn rate and return the value. func (br *BurnRate) GetBurnRate() sdkmath.Int { if br.blockHigh < br.windowSize { return sdkmath.NewInt(br.total).QuoRaw(br.blockHigh) diff --git a/zetaclient/metrics/telemetry.go b/zetaclient/metrics/telemetry.go index 209bf61fed..bdedbd9c50 100644 --- a/zetaclient/metrics/telemetry.go +++ b/zetaclient/metrics/telemetry.go @@ -39,32 +39,35 @@ func NewTelemetryServer() *TelemetryServer { return hs } -// setter/getter for p2pid +// SetP2PID sets p2pid func (t *TelemetryServer) SetP2PID(p2pid string) { t.mu.Lock() t.p2pid = p2pid t.mu.Unlock() } +// GetP2PID gets p2pid func (t *TelemetryServer) GetP2PID() string { t.mu.Lock() defer t.mu.Unlock() return t.p2pid } -// setter/getter for p2pid +// SetIPAddress sets p2pid func (t *TelemetryServer) SetIPAddress(ip string) { t.mu.Lock() t.ipAddress = ip t.mu.Unlock() } +// GetIPAddress gets p2pid func (t *TelemetryServer) GetIPAddress() string { t.mu.Lock() defer t.mu.Unlock() return t.ipAddress } +// AddFeeEntry adds fee entry func (t *TelemetryServer) AddFeeEntry(block int64, amount int64) { t.mu.Lock() err := t.HotKeyBurnRate.AddFee(amount, block) @@ -74,7 +77,7 @@ func (t *TelemetryServer) AddFeeEntry(block int64, amount int64) { t.mu.Unlock() } -// NewHandler registers the API routes and returns a new HTTP handler +// Handlers registers the API routes and returns a new HTTP handler func (t *TelemetryServer) Handlers() http.Handler { router := mux.NewRouter() router.Handle("/ping", http.HandlerFunc(t.pingHandler)).Methods(http.MethodGet) @@ -82,13 +85,8 @@ func (t *TelemetryServer) Handlers() http.Handler { router.Handle("/ip", http.HandlerFunc(t.ipHandler)).Methods(http.MethodGet) router.Handle("/hotkeyburnrate", http.HandlerFunc(t.hotKeyFeeBurnRate)).Methods(http.MethodGet) - // router.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) - // router.Handle("/debug/pprof/heap", pprof.Handler("heap")) - // router.HandleFunc("/debug/pprof/", pprof.Index) - // router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - - //router.Handle("/pending", http.HandlerFunc(t.pendingHandler)).Methods(http.MethodGet) router.Use(logMiddleware()) + return router } @@ -97,7 +95,7 @@ func (t *TelemetryServer) Start() error { return errors.New("invalid http server instance") } if err := t.s.ListenAndServe(); err != nil { - if err != http.ErrServerClosed { + if !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("fail to start http server: %w", err) } } @@ -105,20 +103,6 @@ func (t *TelemetryServer) Start() error { return nil } -func logMiddleware() mux.MiddlewareFunc { - return func(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Debug(). - Str("route", r.URL.Path). - Str("port", r.URL.Port()). - Str("method", r.Method). - Msg("HTTP request received") - - handler.ServeHTTP(w, r) - }) - } -} - func (t *TelemetryServer) Stop() error { c, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -153,3 +137,17 @@ func (t *TelemetryServer) hotKeyFeeBurnRate(w http.ResponseWriter, _ *http.Reque defer t.mu.Unlock() fmt.Fprintf(w, "%v", t.HotKeyBurnRate.GetBurnRate()) } + +func logMiddleware() mux.MiddlewareFunc { + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Debug(). + Str("route", r.URL.Path). + Str("port", r.URL.Port()). + Str("method", r.Method). + Msg("HTTP request received") + + handler.ServeHTTP(w, r) + }) + } +} diff --git a/zetaclient/supplychecker/logger.go b/zetaclient/supplychecker/logger.go new file mode 100644 index 0000000000..dc80c3e490 --- /dev/null +++ b/zetaclient/supplychecker/logger.go @@ -0,0 +1,29 @@ +package supplychecker + +import ( + sdkmath "cosmossdk.io/math" + "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/zetaclient/bitcoin" +) + +// ZetaSupplyCheckLogs is a struct to log the output of the ZetaSupplyChecker +type ZetaSupplyCheckLogs struct { + Logger zerolog.Logger + AbortedTxAmounts sdkmath.Int `json:"aborted_tx_amounts"` + ZetaInTransit sdkmath.Int `json:"zeta_in_transit"` + ExternalChainTotalSupply sdkmath.Int `json:"external_chain_total_supply"` + ZetaTokenSupplyOnNode sdkmath.Int `json:"zeta_token_supply_on_node"` + EthLockedAmount sdkmath.Int `json:"eth_locked_amount"` + NodeAmounts sdkmath.Int `json:"node_amounts"` + LHS sdkmath.Int `json:"LHS"` + RHS sdkmath.Int `json:"RHS"` + SupplyCheckSuccess bool `json:"supply_check_success"` +} + +func (z ZetaSupplyCheckLogs) LogOutput() { + output, err := bitcoin.PrettyPrintStruct(z) + if err != nil { + z.Logger.Error().Err(err).Msgf("error pretty printing struct") + } + z.Logger.Info().Msgf(output) +} diff --git a/zetaclient/supplychecker/validate.go b/zetaclient/supplychecker/validate.go new file mode 100644 index 0000000000..089a34777e --- /dev/null +++ b/zetaclient/supplychecker/validate.go @@ -0,0 +1,30 @@ +package supplychecker + +import ( + sdkmath "cosmossdk.io/math" + "github.com/rs/zerolog" +) + +func ValidateZetaSupply(logger zerolog.Logger, abortedTxAmounts, zetaInTransit, genesisAmounts, externalChainTotalSupply, zetaTokenSupplyOnNode, ethLockedAmount sdkmath.Int) bool { + lhs := ethLockedAmount.Sub(abortedTxAmounts) + rhs := zetaTokenSupplyOnNode.Add(zetaInTransit).Add(externalChainTotalSupply).Sub(genesisAmounts) + + copyZetaTokenSupplyOnNode := zetaTokenSupplyOnNode + copyGenesisAmounts := genesisAmounts + nodeAmounts := copyZetaTokenSupplyOnNode.Sub(copyGenesisAmounts) + logs := ZetaSupplyCheckLogs{ + Logger: logger, + AbortedTxAmounts: abortedTxAmounts, + ZetaInTransit: zetaInTransit, + ExternalChainTotalSupply: externalChainTotalSupply, + NodeAmounts: nodeAmounts, + ZetaTokenSupplyOnNode: zetaTokenSupplyOnNode, + EthLockedAmount: ethLockedAmount, + LHS: lhs, + RHS: rhs, + } + defer logs.LogOutput() + + logs.SupplyCheckSuccess = lhs.Equal(rhs) + return logs.SupplyCheckSuccess +} diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index 14b4c056d2..7e628247d9 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -4,7 +4,6 @@ import ( "fmt" appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" - "github.com/zeta-chain/zetacore/zetaclient/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/interfaces" "github.com/zeta-chain/zetacore/zetaclient/zetabridge" @@ -22,6 +21,7 @@ import ( clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) +// ZetaSupplyChecker is a utility to check the total supply of Zeta tokens type ZetaSupplyChecker struct { coreContext *corecontext.ZetaCoreContext evmClient map[int64]*ethclient.Client @@ -34,7 +34,12 @@ type ZetaSupplyChecker struct { genesisSupply sdkmath.Int } -func NewZetaSupplyChecker(appContext *appcontext.AppContext, zetaClient *zetabridge.ZetaCoreBridge, logger zerolog.Logger) (ZetaSupplyChecker, error) { +// NewZetaSupplyChecker creates a new ZetaSupplyChecker +func NewZetaSupplyChecker( + appContext *appcontext.AppContext, + zetaClient *zetabridge.ZetaCoreBridge, + logger zerolog.Logger, +) (ZetaSupplyChecker, error) { dynamicTicker, err := clienttypes.NewDynamicTicker("ZETASupplyTicker", 15) if err != nil { return ZetaSupplyChecker{}, err @@ -50,6 +55,7 @@ func NewZetaSupplyChecker(appContext *appcontext.AppContext, zetaClient *zetabri coreContext: appContext.ZetaCoreContext(), zetaClient: zetaClient, } + for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { if evmConfig.Chain.IsZetaChain() { continue @@ -70,10 +76,12 @@ func NewZetaSupplyChecker(appContext *appcontext.AppContext, zetaClient *zetabri zetaSupplyChecker.ethereumChain = *chain } } + balances, err := zetaSupplyChecker.zetaClient.GetGenesisSupply() if err != nil { return zetaSupplyChecker, err } + tokensMintedAtBeginBlock, ok := sdkmath.NewIntFromString("200000000000000000") if !ok { return zetaSupplyChecker, fmt.Errorf("error parsing tokens minted at begin block") @@ -84,6 +92,7 @@ func NewZetaSupplyChecker(appContext *appcontext.AppContext, zetaClient *zetabri return zetaSupplyChecker, nil } + func (zs *ZetaSupplyChecker) Start() { defer zs.ticker.Stop() for { @@ -119,15 +128,18 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { if err != nil { return err } + totalSupply, err := zetatokenNonEth.TotalSupply(nil) if err != nil { return err } + totalSupplyInt, ok := sdkmath.NewIntFromString(totalSupply.String()) if !ok { zs.logger.Error().Msgf("error parsing total supply for chain %d", chain.ChainId) continue } + externalChainTotalSupply = externalChainTotalSupply.Add(totalSupplyInt) } @@ -135,6 +147,7 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { if !ok { return fmt.Errorf("eth config not found for chain id %d", zs.ethereumChain.ChainId) } + ethConnectorAddressString := evmChainParams.ConnectorContractAddress ethConnectorAddress := ethcommon.HexToAddress(ethConnectorAddressString) ethConnectorContract, err := evm.FetchConnectorContractEth(ethConnectorAddress, zs.evmClient[zs.ethereumChain.ChainId]) @@ -146,6 +159,7 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { if err != nil { return err } + ethLockedAmountInt, ok := sdkmath.NewIntFromString(ethLockedAmount.String()) if !ok { return fmt.Errorf("error parsing eth locked amount") @@ -156,60 +170,15 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { if err != nil { return err } + abortedAmount, err := zs.AbortedTxAmount() if err != nil { return err } - ValidateZetaSupply(zs.logger, abortedAmount, zetaInTransit, zs.genesisSupply, externalChainTotalSupply, zetaTokenSupplyOnNode, ethLockedAmountInt) - return nil -} -type ZetaSupplyCheckLogs struct { - Logger zerolog.Logger - AbortedTxAmounts sdkmath.Int `json:"aborted_tx_amounts"` - ZetaInTransit sdkmath.Int `json:"zeta_in_transit"` - ExternalChainTotalSupply sdkmath.Int `json:"external_chain_total_supply"` - ZetaTokenSupplyOnNode sdkmath.Int `json:"zeta_token_supply_on_node"` - EthLockedAmount sdkmath.Int `json:"eth_locked_amount"` - NodeAmounts sdkmath.Int `json:"node_amounts"` - LHS sdkmath.Int `json:"LHS"` - RHS sdkmath.Int `json:"RHS"` - SupplyCheckSuccess bool `json:"supply_check_success"` -} - -func (z ZetaSupplyCheckLogs) LogOutput() { - output, err := bitcoin.PrettyPrintStruct(z) - if err != nil { - z.Logger.Error().Err(err).Msgf("error pretty printing struct") - } - z.Logger.Info().Msgf(output) -} + ValidateZetaSupply(zs.logger, abortedAmount, zetaInTransit, zs.genesisSupply, externalChainTotalSupply, zetaTokenSupplyOnNode, ethLockedAmountInt) -func ValidateZetaSupply(logger zerolog.Logger, abortedTxAmounts, zetaInTransit, genesisAmounts, externalChainTotalSupply, zetaTokenSupplyOnNode, ethLockedAmount sdkmath.Int) bool { - lhs := ethLockedAmount.Sub(abortedTxAmounts) - rhs := zetaTokenSupplyOnNode.Add(zetaInTransit).Add(externalChainTotalSupply).Sub(genesisAmounts) - - copyZetaTokenSupplyOnNode := zetaTokenSupplyOnNode - copyGenesisAmounts := genesisAmounts - nodeAmounts := copyZetaTokenSupplyOnNode.Sub(copyGenesisAmounts) - logs := ZetaSupplyCheckLogs{ - Logger: logger, - AbortedTxAmounts: abortedTxAmounts, - ZetaInTransit: zetaInTransit, - ExternalChainTotalSupply: externalChainTotalSupply, - NodeAmounts: nodeAmounts, - ZetaTokenSupplyOnNode: zetaTokenSupplyOnNode, - EthLockedAmount: ethLockedAmount, - LHS: lhs, - RHS: rhs, - } - defer logs.LogOutput() - if !lhs.Equal(rhs) { - logs.SupplyCheckSuccess = false - return false - } - logs.SupplyCheckSuccess = true - return true + return nil } func (zs *ZetaSupplyChecker) AbortedTxAmount() (sdkmath.Int, error) { @@ -229,6 +198,7 @@ func (zs *ZetaSupplyChecker) GetAmountOfZetaInTransit() sdkmath.Int { chainsToCheck = append(append(chainsToCheck, zs.externalEvmChain...), zs.ethereumChain) cctxs := zs.GetPendingCCTXInTransit(chainsToCheck) amount := sdkmath.ZeroUint() + for _, cctx := range cctxs { amount = amount.Add(cctx.GetCurrentOutTxParam().Amount) } @@ -236,8 +206,10 @@ func (zs *ZetaSupplyChecker) GetAmountOfZetaInTransit() sdkmath.Int { if !ok { panic("error parsing amount") } + return amountInt } + func (zs *ZetaSupplyChecker) GetPendingCCTXInTransit(receivingChains []chains.Chain) []*types.CrossChainTx { cctxInTransit := make([]*types.CrossChainTx, 0) for _, chain := range receivingChains { @@ -266,5 +238,6 @@ func (zs *ZetaSupplyChecker) GetPendingCCTXInTransit(receivingChains []chains.Ch } } } + return cctxInTransit } diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index 380c3ab501..ad8302577d 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -3,16 +3,20 @@ package testutils import ethcommon "github.com/ethereum/go-ethereum/common" const ( - // tss addresses + // TSSAddressEVMMainnet the EVM TSS address for test purposes + // Note: public key is zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc TSSAddressEVMMainnet = "0x70e967acFcC17c3941E87562161406d41676FD83" + + // TSSAddressBTCMainnet the BTC TSS address for test purposes TSSAddressBTCMainnet = "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y" - TssPubkeyEVMMainnet = "zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc" + // TSSAddressEVMAthens3 the EVM TSS address for test purposes + // Note: public key is zetapub1addwnpepq28c57cvcs0a2htsem5zxr6qnlvq9mzhmm76z3jncsnzz32rclangr2g35p TSSAddressEVMAthens3 = "0x8531a5aB847ff5B22D855633C25ED1DA3255247e" + + // TSSAddressBTCAthens3 the BTC TSS address for test purposes TSSAddressBTCAthens3 = "tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur" - TssPubkeyEVMAthens3 = "zetapub1addwnpepq28c57cvcs0a2htsem5zxr6qnlvq9mzhmm76z3jncsnzz32rclangr2g35p" - // some other addresses OtherAddress1 = "0x21248Decd0B7EcB0F30186297766b8AB6496265b" OtherAddress2 = "0x33A351C90aF486AebC35042Bb0544123cAed26AB" OtherAddress3 = "0x86B77E4fBd07CFdCc486cAe4F2787fB5C5a62cd3" @@ -27,8 +31,10 @@ const ( // ConnectorAddresses contains constants ERC20 connector addresses for testing var ConnectorAddresses = map[int64]ethcommon.Address{ - // mainnet - 1: ethcommon.HexToAddress("0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a"), + // Connector address on Ethereum mainnet + 1: ethcommon.HexToAddress("0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a"), + + // Connector address on Binance Smart Chain mainnet 56: ethcommon.HexToAddress("0x000063A6e758D9e2f438d430108377564cf4077D"), // testnet @@ -42,8 +48,10 @@ var ConnectorAddresses = map[int64]ethcommon.Address{ // CustodyAddresses contains constants ERC20 custody addresses for testing var CustodyAddresses = map[int64]ethcommon.Address{ - // mainnet - 1: ethcommon.HexToAddress("0x0000030Ec64DF25301d8414eE5a29588C4B0dE10"), + // ERC20 custody address on Ethereum mainnet + 1: ethcommon.HexToAddress("0x0000030Ec64DF25301d8414eE5a29588C4B0dE10"), + + // ERC20 custody address on Binance Smart Chain mainnet 56: ethcommon.HexToAddress("0x00000fF8fA992424957F97688015814e707A0115"), // testnet diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index 001ca2b07b..42be79028b 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -35,19 +35,6 @@ func cloneCctx(t *testing.T, cctx *crosschaintypes.CrossChainTx) *crosschaintype return cloned } -// SaveObjectToJSONFile saves an object to a file in JSON format -func SaveObjectToJSONFile(obj interface{}, filename string) error { - file, err := os.Create(filepath.Clean(filename)) - if err != nil { - return err - } - defer file.Close() - - // write the struct to the file - encoder := json.NewEncoder(file) - return encoder.Encode(obj) -} - // LoadObjectFromJSONFile loads an object from a file in JSON format func LoadObjectFromJSONFile(t *testing.T, obj interface{}, filename string) { file, err := os.Open(filepath.Clean(filename)) @@ -66,27 +53,6 @@ func ComplianceConfigTest() config.ComplianceConfig { } } -// SaveTrimedEVMBlockTrimTxInput trims tx input data from a block and saves it to a file -func SaveEVMBlockTrimTxInput(block *ethrpc.Block, filename string) error { - for i := range block.Transactions { - block.Transactions[i].Input = "0x" - } - return SaveObjectToJSONFile(block, filename) -} - -// SaveTrimedBTCBlockTrimTx trims tx data from a block and saves it to a file -func SaveBTCBlockTrimTx(blockVb *btcjson.GetBlockVerboseTxResult, filename string) error { - for i := range blockVb.Tx { - // reserve one coinbase tx and one non-coinbase tx - if i >= 2 { - blockVb.Tx[i].Hex = "" - blockVb.Tx[i].Vin = nil - blockVb.Tx[i].Vout = nil - } - } - return SaveObjectToJSONFile(blockVb, filename) -} - // LoadCctxByIntx loads archived cctx by intx func LoadCctxByIntx( t *testing.T, @@ -234,7 +200,7 @@ func LoadEVMIntxNReceiptDonation( return tx, receipt } -// LoadTxNReceiptNCctx loads archived intx, receipt and corresponding cctx from file +// LoadEVMIntxNReceiptNCctx loads archived intx, receipt and corresponding cctx from file func LoadEVMIntxNReceiptNCctx( t *testing.T, chainID int64, @@ -288,7 +254,7 @@ func LoadEVMOuttxNReceipt( return tx, receipt } -// LoadEVMOuttxNReceiptNEvent loads archived cctx, outtx and receipt from file +// LoadEVMCctxNOuttxNReceipt loads archived cctx, outtx and receipt from file func LoadEVMCctxNOuttxNReceipt( t *testing.T, chainID int64, @@ -301,3 +267,40 @@ func LoadEVMCctxNOuttxNReceipt( receipt := LoadEVMOuttxReceipt(t, chainID, txHash, coinType, eventName) return cctx, outtx, receipt } + +// SaveObjectToJSONFile saves an object to a file in JSON format +// NOTE: this function is not used in the tests but used when creating test data +func SaveObjectToJSONFile(obj interface{}, filename string) error { + file, err := os.Create(filepath.Clean(filename)) + if err != nil { + return err + } + defer file.Close() + + // write the struct to the file + encoder := json.NewEncoder(file) + return encoder.Encode(obj) +} + +// SaveEVMBlockTrimTxInput trims tx input data from a block and saves it to a file +// NOTE: this function is not used in the tests but used when creating test data +func SaveEVMBlockTrimTxInput(block *ethrpc.Block, filename string) error { + for i := range block.Transactions { + block.Transactions[i].Input = "0x" + } + return SaveObjectToJSONFile(block, filename) +} + +// SaveBTCBlockTrimTx trims tx data from a block and saves it to a file +// NOTE: this function is not used in the tests but used when creating test data +func SaveBTCBlockTrimTx(blockVb *btcjson.GetBlockVerboseTxResult, filename string) error { + for i := range blockVb.Tx { + // reserve one coinbase tx and one non-coinbase tx + if i >= 2 { + blockVb.Tx[i].Hex = "" + blockVb.Tx[i].Vin = nil + blockVb.Tx[i].Vout = nil + } + } + return SaveObjectToJSONFile(blockVb, filename) +} diff --git a/zetaclient/tss/tss_keysign_manager.go b/zetaclient/tss/concurrent_keysigns_tracker.go similarity index 100% rename from zetaclient/tss/tss_keysign_manager.go rename to zetaclient/tss/concurrent_keysigns_tracker.go diff --git a/zetaclient/tss/tss_keysign_manager_test.go b/zetaclient/tss/concurrent_keysigns_tracker_test.go similarity index 100% rename from zetaclient/tss/tss_keysign_manager_test.go rename to zetaclient/tss/concurrent_keysigns_tracker_test.go diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index 3ff467c2dd..379d89c5f7 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -41,8 +41,8 @@ const ( ) type Key struct { - PubkeyInBytes []byte // FIXME: compressed pubkey? - PubkeyInBech32 string // FIXME: same above + PubkeyInBytes []byte + PubkeyInBech32 string AddressInHex string } @@ -55,12 +55,15 @@ func NewTSSKey(pk string) (*Key, error) { log.Error().Err(err).Msgf("GetPubKeyFromBech32 from %s", pk) return nil, fmt.Errorf("GetPubKeyFromBech32: %w", err) } + decompresspubkey, err := crypto.DecompressPubkey(pubkey.Bytes()) if err != nil { return nil, fmt.Errorf("NewTSS: DecompressPubkey error: %w", err) } + TSSKey.PubkeyInBytes = crypto.FromECDSAPub(decompresspubkey) TSSKey.AddressInHex = crypto.PubkeyToAddress(*decompresspubkey).Hex() + return TSSKey, nil } @@ -98,6 +101,7 @@ func NewTSS( if err != nil { return nil, fmt.Errorf("SetupTSSServer error: %w", err) } + newTss := TSS{ Server: server, Keys: make(map[string]*Key), @@ -112,14 +116,17 @@ func NewTSS( if err != nil { return nil, err } + _, pubkeyInBech32, err := keys.GetKeyringKeybase(appContext.Config(), hotkeyPassword) if err != nil { return nil, err } + err = newTss.VerifyKeysharesForPubkeys(tssHistoricalList, pubkeyInBech32) if err != nil { bridge.GetLogger().Error().Err(err).Msg("VerifyKeysharesForPubkeys fail") } + keygenRes, err := newTss.CoreBridge.GetKeyGen() if err != nil { return nil, err @@ -134,7 +141,13 @@ func NewTSS( return &newTss, nil } -func SetupTSSServer(peer p2p.AddrList, privkey tmcrypto.PrivKey, preParams *keygen.LocalPreParams, cfg config.Config, tssPassword string) (*tss.TssServer, error) { +func SetupTSSServer( + peer p2p.AddrList, + privkey tmcrypto.PrivKey, + preParams *keygen.LocalPreParams, + cfg config.Config, + tssPassword string, +) (*tss.TssServer, error) { bootstrapPeers := peer log.Info().Msgf("Peers AddrList %v", bootstrapPeers) @@ -149,10 +162,12 @@ func SetupTSSServer(peer p2p.AddrList, privkey tmcrypto.PrivKey, preParams *keyg tsspath = path.Join(homedir, ".Tss") log.Info().Msgf("create temporary TSSPATH: %s", tsspath) } + IP := cfg.PublicIP if len(IP) == 0 { log.Info().Msg("empty public IP in config") } + tssServer, err := tss.NewTss( bootstrapPeers, 6668, @@ -192,7 +207,6 @@ func SetupTSSServer(peer p2p.AddrList, privkey tmcrypto.PrivKey, preParams *keyg return tssServer, nil } -// FIXME: does it return pubkey in compressed form or uncompressed? func (tss *TSS) Pubkey() []byte { return tss.Keys[tss.CurrentPubkey].PubkeyInBytes } @@ -208,6 +222,7 @@ func (tss *TSS) Sign(digest []byte, height uint64, nonce uint64, chain *chains.C if optionalPubKey != "" { tssPubkey = optionalPubKey } + // #nosec G701 always in range keysignReq := keysign.NewRequest(tssPubkey, []string{base64.StdEncoding.EncodeToString(H)}, int64(height), nil, "0.14.0") tss.KeysignsTracker.StartMsgSign() @@ -216,6 +231,7 @@ func (tss *TSS) Sign(digest []byte, height uint64, nonce uint64, chain *chains.C if err != nil { log.Warn().Msg("keysign fail") } + if ksRes.Status == thorcommon.Fail { log.Warn().Msgf("keysign status FAIL posting blame to core, blaming node(s): %#v", ksRes.Blame.BlameNodes) @@ -246,21 +262,25 @@ func (tss *TSS) Sign(digest []byte, height uint64, nonce uint64, chain *chains.C log.Warn().Err(err).Msgf("signature has length 0") return [65]byte{}, fmt.Errorf("keysign fail: %s", err) } + if !verifySignature(tssPubkey, signature, H) { log.Error().Err(err).Msgf("signature verification failure") return [65]byte{}, fmt.Errorf("signuature verification fail") } + var sigbyte [65]byte _, err = base64.StdEncoding.Decode(sigbyte[:32], []byte(signature[0].R)) if err != nil { log.Error().Err(err).Msg("decoding signature R") return [65]byte{}, fmt.Errorf("signuature verification fail") } + _, err = base64.StdEncoding.Decode(sigbyte[32:64], []byte(signature[0].S)) if err != nil { log.Error().Err(err).Msg("decoding signature S") return [65]byte{}, fmt.Errorf("signuature verification fail") } + _, err = base64.StdEncoding.Decode(sigbyte[64:65], []byte(signature[0].RecoveryID)) if err != nil { log.Error().Err(err).Msg("decoding signature RecoveryID") @@ -372,12 +392,15 @@ func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chain * func (tss *TSS) Validate() error { evmAddress := tss.EVMAddress() blankAddress := ethcommon.Address{} + if evmAddress == blankAddress { return fmt.Errorf("invalid evm address : %s", evmAddress.String()) } + if tss.BTCAddressWitnessPubkeyHash() == nil { return fmt.Errorf("invalid btc pub key hash : %s", tss.BTCAddress()) } + return nil } @@ -438,6 +461,7 @@ func (tss *TSS) VerifyKeysharesForPubkeys(tssList []observertypes.TSS, granteePu } return nil } + func (tss *TSS) LoadTssFilesFromDirectory(tssPath string) error { files, err := os.ReadDir(tssPath) if err != nil { @@ -445,12 +469,14 @@ func (tss *TSS) LoadTssFilesFromDirectory(tssPath string) error { return err } found := false + var sharefiles []os.DirEntry for _, file := range files { if !file.IsDir() && strings.HasPrefix(filepath.Base(file.Name()), "localstate") { sharefiles = append(sharefiles, file) } } + if len(sharefiles) > 0 { sort.SliceStable(sharefiles, func(i, j int) bool { fi, err := sharefiles[i].Info() @@ -480,6 +506,7 @@ func (tss *TSS) LoadTssFilesFromDirectory(tssPath string) error { } } } + if !found { log.Info().Msg("TSS Keyshare file NOT found") } @@ -527,6 +554,7 @@ func TestKeysign(tssPubkey string, tssServer *tss.TssServer) error { if err != nil { log.Warn().Msg("keysign fail") } + signature := ksRes.Signatures // [{cyP8i/UuCVfQKDsLr1kpg09/CeIHje1FU6GhfmyMD5Q= D4jXTH3/CSgCg+9kLjhhfnNo3ggy9DTQSlloe3bbKAs= eY++Z2LwsuKG1JcghChrsEJ4u9grLloaaFZNtXI3Ujk= AA==}] // 32B msg hash, 32B R, 32B S, 1B RC @@ -536,13 +564,20 @@ func TestKeysign(tssPubkey string, tssServer *tss.TssServer) error { log.Info().Msgf("signature has length 0, skipping verify") return fmt.Errorf("signature has length 0") } + verifySignature(tssPubkey, signature, H.Bytes()) if verifySignature(tssPubkey, signature, H.Bytes()) { return nil } + return fmt.Errorf("verify signature fail") } +func IsEnvFlagEnabled(flag string) bool { + value := os.Getenv(flag) + return value == "true" || value == "1" +} + func verifySignature(tssPubkey string, signature []keysign.Signature, H []byte) bool { if len(signature) == 0 { log.Warn().Msg("verify_signature: empty signature array") @@ -552,6 +587,7 @@ func verifySignature(tssPubkey string, signature []keysign.Signature, H []byte) if err != nil { log.Error().Msg("get pubkey from bech32 fail") } + // verify the signature of msg. var sigbyte [65]byte _, err = base64.StdEncoding.Decode(sigbyte[:32], []byte(signature[0].R)) @@ -559,21 +595,25 @@ func verifySignature(tssPubkey string, signature []keysign.Signature, H []byte) log.Error().Err(err).Msg("decoding signature R") return false } + _, err = base64.StdEncoding.Decode(sigbyte[32:64], []byte(signature[0].S)) if err != nil { log.Error().Err(err).Msg("decoding signature S") return false } + _, err = base64.StdEncoding.Decode(sigbyte[64:65], []byte(signature[0].RecoveryID)) if err != nil { log.Error().Err(err).Msg("decoding signature RecoveryID") return false } + sigPublicKey, err := crypto.SigToPub(H, sigbyte[:]) if err != nil { log.Error().Err(err).Msg("SigToPub error in verify_signature") return false } + compressedPubkey := crypto.CompressPubkey(sigPublicKey) log.Info().Msgf("pubkey %s recovered pubkey %s", pubkey.String(), hex.EncodeToString(compressedPubkey)) return bytes.Equal(pubkey.Bytes(), compressedPubkey) @@ -611,8 +651,3 @@ func getKeyAddrBTCWitnessPubkeyHash(tssPubkey string, chainID int64) (*btcutil.A } return addr, nil } - -func IsEnvFlagEnabled(flag string) bool { - value := os.Getenv(flag) - return value == "true" || value == "1" -} diff --git a/zetaclient/tss/tss_signer_test.go b/zetaclient/tss/tss_signer_test.go index 4388fc5ef1..d19e2effcc 100644 --- a/zetaclient/tss/tss_signer_test.go +++ b/zetaclient/tss/tss_signer_test.go @@ -5,15 +5,26 @@ import ( "os" "testing" - "github.com/zeta-chain/zetacore/zetaclient/keys" - "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/cmd" "github.com/zeta-chain/zetacore/pkg/cosmos" "github.com/zeta-chain/zetacore/pkg/crypto" ) +func setupConfig() { + testConfig := sdk.GetConfig() + testConfig.SetBech32PrefixForAccount(cmd.Bech32PrefixAccAddr, cmd.Bech32PrefixAccPub) + testConfig.SetBech32PrefixForValidator(cmd.Bech32PrefixValAddr, cmd.Bech32PrefixValPub) + testConfig.SetBech32PrefixForConsensusNode(cmd.Bech32PrefixConsAddr, cmd.Bech32PrefixConsPub) + testConfig.SetFullFundraiserPath(cmd.ZetaChainHDPath) + sdk.SetCoinDenomRegex(func() string { + return cmd.DenomRegex + }) +} + func Test_LoadTssFilesFromDirectory(t *testing.T) { tt := []struct { @@ -52,7 +63,7 @@ func Test_LoadTssFilesFromDirectory(t *testing.T) { } func GenerateKeyshareFiles(n int, dir string) error { - keys.SetupConfigForTest() + setupConfig() err := os.Chdir(dir) if err != nil { return err diff --git a/zetaclient/types/account_resp.go b/zetaclient/types/account_resp.go deleted file mode 100644 index 41424907ad..0000000000 --- a/zetaclient/types/account_resp.go +++ /dev/null @@ -1,12 +0,0 @@ -package types - -// AccountResp the response from thorclient -type AccountResp struct { - Height string `json:"height"` - Result struct { - Value struct { - AccountNumber uint64 `json:"account_number,string"` - Sequence uint64 `json:"sequence,string"` - } `json:"value"` - } `json:"result"` -} diff --git a/zetaclient/types/ethish_test.go b/zetaclient/types/ethish_test.go deleted file mode 100644 index d932b46501..0000000000 --- a/zetaclient/types/ethish_test.go +++ /dev/null @@ -1 +0,0 @@ -package types_test diff --git a/zetaclient/zetabridge/block_height.go b/zetaclient/zetabridge/block_height.go deleted file mode 100644 index 30fcce82b4..0000000000 --- a/zetaclient/zetabridge/block_height.go +++ /dev/null @@ -1,24 +0,0 @@ -package zetabridge - -import ( - "context" - "fmt" - - "github.com/zeta-chain/zetacore/x/crosschain/types" -) - -// GetBlockHeight returns the current height for metachain blocks -// FIXME: deprecate this in favor of tendermint RPC? -func (b *ZetaCoreBridge) GetBlockHeight() (int64, error) { - client := types.NewQueryClient(b.grpcConn) - height, err := client.LastZetaHeight( - context.Background(), - &types.QueryLastZetaHeightRequest{}, - ) - if err != nil { - return 0, err - } - - fmt.Printf("block height: %d\n", height.Height) - return height.Height, nil -} diff --git a/zetaclient/zetabridge/query_test.go b/zetaclient/zetabridge/query_test.go index ea76fa78fd..556bb83101 100644 --- a/zetaclient/zetabridge/query_test.go +++ b/zetaclient/zetabridge/query_test.go @@ -438,12 +438,6 @@ func TestZetaCoreBridge_GetZetaBlockHeight(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedOutput.Height, resp) }) - - t.Run("get block height success", func(t *testing.T) { - resp, err := zetabridge.GetBlockHeight() - require.NoError(t, err) - require.Equal(t, expectedOutput.Height, resp) - }) } func TestZetaCoreBridge_GetBaseGasPrice(t *testing.T) { diff --git a/zetaclient/zetabridge/zetacore_bridge.go b/zetaclient/zetabridge/zetacore_bridge.go index 3899d51d82..5da5b592f1 100644 --- a/zetaclient/zetabridge/zetacore_bridge.go +++ b/zetaclient/zetabridge/zetacore_bridge.go @@ -5,18 +5,13 @@ import ( "sync" "time" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/simapp/params" - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/rs/zerolog" "github.com/rs/zerolog/log" rpcclient "github.com/tendermint/tendermint/rpc/client" "github.com/zeta-chain/zetacore/app" "github.com/zeta-chain/zetacore/pkg/authz" "github.com/zeta-chain/zetacore/pkg/chains" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/config" corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" @@ -110,16 +105,6 @@ func NewZetaCoreBridge( }, nil } -// MakeLegacyCodec creates codec -func MakeLegacyCodec() *codec.LegacyAmino { - cdc := codec.NewLegacyAmino() - banktypes.RegisterLegacyAminoCodec(cdc) - authtypes.RegisterLegacyAminoCodec(cdc) - sdk.RegisterLegacyAminoCodec(cdc) - crosschaintypes.RegisterCodec(cdc) - return cdc -} - func (b *ZetaCoreBridge) GetLogger() *zerolog.Logger { return &b.logger } diff --git a/zetaclient/zetacore_observer.go b/zetaclient/zetacore_observer.go index c66f15f50a..5f187caed7 100644 --- a/zetaclient/zetacore_observer.go +++ b/zetaclient/zetacore_observer.go @@ -89,6 +89,61 @@ func (co *CoreObserver) MonitorCore(appContext *appcontext.AppContext) { }() } +// GetUpdatedSigner returns signer with updated chain parameters +func (co *CoreObserver) GetUpdatedSigner(coreContext *corecontext.ZetaCoreContext, chainID int64) (interfaces.ChainSigner, error) { + signer, found := co.signerMap[chainID] + if !found { + return nil, fmt.Errorf("signer not found for chainID %d", chainID) + } + // update EVM signer parameters only. BTC signer doesn't use chain parameters for now. + if chains.IsEVMChain(chainID) { + evmParams, found := coreContext.GetEVMChainParams(chainID) + if found { + // update zeta connector and ERC20 custody addresses + zetaConnectorAddress := ethcommon.HexToAddress(evmParams.GetConnectorContractAddress()) + erc20CustodyAddress := ethcommon.HexToAddress(evmParams.GetErc20CustodyContractAddress()) + if zetaConnectorAddress != signer.GetZetaConnectorAddress() { + signer.SetZetaConnectorAddress(zetaConnectorAddress) + co.logger.ZetaChainWatcher.Info().Msgf( + "updated zeta connector address for chainID %d, new address: %s", chainID, zetaConnectorAddress) + } + if erc20CustodyAddress != signer.GetERC20CustodyAddress() { + signer.SetERC20CustodyAddress(erc20CustodyAddress) + co.logger.ZetaChainWatcher.Info().Msgf( + "updated ERC20 custody address for chainID %d, new address: %s", chainID, erc20CustodyAddress) + } + } + } + return signer, nil +} + +// GetUpdatedChainClient returns chain client object with updated chain parameters +func (co *CoreObserver) GetUpdatedChainClient(coreContext *corecontext.ZetaCoreContext, chainID int64) (interfaces.ChainClient, error) { + chainOb, found := co.clientMap[chainID] + if !found { + return nil, fmt.Errorf("chain client not found for chainID %d", chainID) + } + // update chain client chain parameters + curParams := chainOb.GetChainParams() + if chains.IsEVMChain(chainID) { + evmParams, found := coreContext.GetEVMChainParams(chainID) + if found && !observertypes.ChainParamsEqual(curParams, *evmParams) { + chainOb.SetChainParams(*evmParams) + co.logger.ZetaChainWatcher.Info().Msgf( + "updated chain params for chainID %d, new params: %v", chainID, *evmParams) + } + } else if chains.IsBitcoinChain(chainID) { + _, btcParams, found := coreContext.GetBTCChainParams() + + if found && !observertypes.ChainParamsEqual(curParams, *btcParams) { + chainOb.SetChainParams(*btcParams) + co.logger.ZetaChainWatcher.Info().Msgf( + "updated chain params for Bitcoin, new params: %v", *btcParams) + } + } + return chainOb, nil +} + // startCctxScheduler schedules keysigns for cctxs on each ZetaChain block (the ticker) func (co *CoreObserver) startCctxScheduler(appContext *appcontext.AppContext) { outTxMan := outtxprocessor.NewOutTxProcessorManager(co.logger.ChainLogger) @@ -219,7 +274,8 @@ func (co *CoreObserver) scheduleCctxEVM( chainID int64, cctxList []*types.CrossChainTx, ob interfaces.ChainClient, - signer interfaces.ChainSigner) { + signer interfaces.ChainSigner, +) { res, err := co.bridge.GetAllOutTxTrackerByChain(chainID, interfaces.Ascending) if err != nil { co.logger.ZetaChainWatcher.Warn().Err(err).Msgf("scheduleCctxEVM: GetAllOutTxTrackerByChain failed for chain %d", chainID) @@ -305,7 +361,8 @@ func (co *CoreObserver) scheduleCctxBTC( chainID int64, cctxList []*types.CrossChainTx, ob interfaces.ChainClient, - signer interfaces.ChainSigner) { + signer interfaces.ChainSigner, +) { btcClient, ok := ob.(*bitcoin.BTCChainClient) if !ok { // should never happen co.logger.ZetaChainWatcher.Error().Msgf("scheduleCctxBTC: chain client is not a bitcoin client") @@ -353,58 +410,3 @@ func (co *CoreObserver) scheduleCctxBTC( } } } - -// GetUpdatedSigner returns signer with updated chain parameters -func (co *CoreObserver) GetUpdatedSigner(coreContext *corecontext.ZetaCoreContext, chainID int64) (interfaces.ChainSigner, error) { - signer, found := co.signerMap[chainID] - if !found { - return nil, fmt.Errorf("signer not found for chainID %d", chainID) - } - // update EVM signer parameters only. BTC signer doesn't use chain parameters for now. - if chains.IsEVMChain(chainID) { - evmParams, found := coreContext.GetEVMChainParams(chainID) - if found { - // update zeta connector and ERC20 custody addresses - zetaConnectorAddress := ethcommon.HexToAddress(evmParams.GetConnectorContractAddress()) - erc20CustodyAddress := ethcommon.HexToAddress(evmParams.GetErc20CustodyContractAddress()) - if zetaConnectorAddress != signer.GetZetaConnectorAddress() { - signer.SetZetaConnectorAddress(zetaConnectorAddress) - co.logger.ZetaChainWatcher.Info().Msgf( - "updated zeta connector address for chainID %d, new address: %s", chainID, zetaConnectorAddress) - } - if erc20CustodyAddress != signer.GetERC20CustodyAddress() { - signer.SetERC20CustodyAddress(erc20CustodyAddress) - co.logger.ZetaChainWatcher.Info().Msgf( - "updated ERC20 custody address for chainID %d, new address: %s", chainID, erc20CustodyAddress) - } - } - } - return signer, nil -} - -// GetUpdatedChainClient returns chain client object with updated chain parameters -func (co *CoreObserver) GetUpdatedChainClient(coreContext *corecontext.ZetaCoreContext, chainID int64) (interfaces.ChainClient, error) { - chainOb, found := co.clientMap[chainID] - if !found { - return nil, fmt.Errorf("chain client not found for chainID %d", chainID) - } - // update chain client chain parameters - curParams := chainOb.GetChainParams() - if chains.IsEVMChain(chainID) { - evmParams, found := coreContext.GetEVMChainParams(chainID) - if found && !observertypes.ChainParamsEqual(curParams, *evmParams) { - chainOb.SetChainParams(*evmParams) - co.logger.ZetaChainWatcher.Info().Msgf( - "updated chain params for chainID %d, new params: %v", chainID, *evmParams) - } - } else if chains.IsBitcoinChain(chainID) { - _, btcParams, found := coreContext.GetBTCChainParams() - - if found && !observertypes.ChainParamsEqual(curParams, *btcParams) { - chainOb.SetChainParams(*btcParams) - co.logger.ZetaChainWatcher.Info().Msgf( - "updated chain params for Bitcoin, new params: %v", *btcParams) - } - } - return chainOb, nil -}