From 3eba70102a12be9ab55f0939b1a5260251ef2d5c Mon Sep 17 00:00:00 2001 From: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:06:20 -0500 Subject: [PATCH] refactor: remove btc deposit fee v1 and improve unit tests (#2899) * remove CalcDepositorFeeV2; add unit tests; migration mock btc client to Mockery * replace manual btc rpc mock with Mockery * move createRPCClientAndLoadTx to testutils package as CreateBTCRPCAndLoadTx * move GetSenderAddressByVin to inbound.go as it's for inbound observation * add unit test for IsBlockConfirmed * add changelog entry * remove unused constants * move changelog entry under Refactor section; created sub method DecodeSenderFromScript() and added unit tests --- changelog.md | 1 + e2e/runner/bitcoin.go | 2 - zetaclient/chains/base/observer.go | 9 + zetaclient/chains/base/observer_test.go | 49 ++ zetaclient/chains/bitcoin/fee.go | 39 +- zetaclient/chains/bitcoin/observer/inbound.go | 144 +++-- .../chains/bitcoin/observer/inbound_test.go | 249 +++----- .../chains/bitcoin/observer/observer.go | 62 -- .../chains/bitcoin/observer/observer_test.go | 41 +- .../chains/bitcoin/observer/outbound_test.go | 5 +- zetaclient/chains/bitcoin/observer/witness.go | 19 +- .../chains/bitcoin/observer/witness_test.go | 108 +++- .../chains/bitcoin/rpc/rpc_live_test.go | 8 +- zetaclient/chains/bitcoin/tx_script.go | 22 + zetaclient/chains/bitcoin/tx_script_test.go | 214 +++++-- ...bf7417cfc8a4d6f277ec11f40cd87319f04aa.json | 27 + zetaclient/testutils/mocks/btc_rpc.go | 603 ++++++++++++++---- zetaclient/testutils/testdata.go | 11 + zetaclient/testutils/testrpc/rpc_btc.go | 28 + 19 files changed, 1097 insertions(+), 544 deletions(-) create mode 100644 zetaclient/testdata/btc/chain_8332_msgtx_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json diff --git a/changelog.md b/changelog.md index 207dcc1311..fcdee60f57 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ * [2802](https://github.com/zeta-chain/node/pull/2802) - set default liquidity cap for new ZRC20s * [2826](https://github.com/zeta-chain/node/pull/2826) - remove unused code from emissions module and add new parameter for fixed block reward amount * [2890](https://github.com/zeta-chain/node/pull/2890) - refactor `MsgUpdateChainInfo` to accept a single chain, and add `MsgRemoveChainInfo` to remove a chain +* [2899](https://github.com/zeta-chain/node/pull/2899) - remove btc deposit fee v1 and improve unit tests ### Tests diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index 0253dcd6a4..60c6ae3025 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -267,7 +267,6 @@ func (r *E2ERunner) SendToTSSFromDeployerWithMemo( rawtx, err := btcRPC.GetRawTransactionVerbose(txid) require.NoError(r, err) - depositorFee := zetabitcoin.DefaultDepositorFee events, err := btcobserver.FilterAndParseIncomingTx( btcRPC, []btcjson.TxRawResult{*rawtx}, @@ -275,7 +274,6 @@ func (r *E2ERunner) SendToTSSFromDeployerWithMemo( r.BTCTSSAddress.EncodeAddress(), log.Logger, r.BitcoinParams, - depositorFee, ) require.NoError(r, err) r.Logger.Info("bitcoin inbound events:") diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 020fd323f2..5ad8a6990b 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -228,6 +228,15 @@ func (ob *Observer) WithLastBlock(lastBlock uint64) *Observer { return ob } +// IsBlockConfirmed checks if the given block number is confirmed. +// +// Note: block 100 is confirmed if the last block is 100 and confirmation count is 1. +func (ob *Observer) IsBlockConfirmed(blockNumber uint64) bool { + lastBlock := ob.LastBlock() + confBlock := blockNumber + ob.chainParams.ConfirmationCount - 1 + return lastBlock >= confBlock +} + // LastBlockScanned get last block scanned (not necessarily caught up with the chain; could be slow/paused). func (ob *Observer) LastBlockScanned() uint64 { height := atomic.LoadUint64(&ob.lastBlockScanned) diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 01713f6c4c..1bbd1b00b5 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -27,12 +27,16 @@ import ( const ( // defaultAlertLatency is the default alert latency (in seconds) for unit tests defaultAlertLatency = 60 + + // defaultConfirmationCount is the default confirmation count for unit tests + defaultConfirmationCount = 2 ) // createObserver creates a new observer for testing func createObserver(t *testing.T, chain chains.Chain, alertLatency int64) *base.Observer { // constructor parameters chainParams := *sample.ChainParams(chain.ChainId) + chainParams.ConfirmationCount = defaultConfirmationCount zetacoreClient := mocks.NewZetacoreClient(t) tss := mocks.NewTSSMainnet() @@ -267,6 +271,51 @@ func TestObserverGetterAndSetter(t *testing.T) { }) } +func TestIsBlockConfirmed(t *testing.T) { + tests := []struct { + name string + chain chains.Chain + block uint64 + lastBlock uint64 + confirmed bool + }{ + { + name: "should confirm block 100 when confirmation arrives 2", + chain: chains.BitcoinMainnet, + block: 100, + lastBlock: 101, // got 2 confirmations + confirmed: true, + }, + { + name: "should not confirm block 100 when confirmation < 2", + chain: chains.BitcoinMainnet, + block: 100, + lastBlock: 100, // got 1 confirmation, need one more + confirmed: false, + }, + { + name: "should confirm block 100 when confirmation arrives 2", + chain: chains.Ethereum, + block: 100, + lastBlock: 99, // last block lagging behind, need to wait + confirmed: false, + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create observer + ob := createObserver(t, tt.chain, defaultAlertLatency) + ob = ob.WithLastBlock(tt.lastBlock) + + // check if block is confirmed + confirmed := ob.IsBlockConfirmed(tt.block) + require.Equal(t, tt.confirmed, confirmed) + }) + } +} + func TestOutboundID(t *testing.T) { tests := []struct { name string diff --git a/zetaclient/chains/bitcoin/fee.go b/zetaclient/chains/bitcoin/fee.go index fd0b5e7139..58297f37d6 100644 --- a/zetaclient/chains/bitcoin/fee.go +++ b/zetaclient/chains/bitcoin/fee.go @@ -12,7 +12,6 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" - "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" @@ -44,13 +43,6 @@ const ( // feeRateCountBackBlocks is the default number of blocks to look back for fee rate estimation feeRateCountBackBlocks = 2 - - // DynamicDepositorFeeHeight is the mainnet height from which dynamic depositor fee V1 is applied - DynamicDepositorFeeHeight = 834500 - - // DynamicDepositorFeeHeightV2 is the mainnet height from which dynamic depositor fee V2 is applied - // Height 863400 is approximately a month away (2024-09-28) from the time of writing, allowing enough time for the upgrade - DynamicDepositorFeeHeightV2 = 863400 ) var ( @@ -226,37 +218,8 @@ func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *ch return txsFees / int64(vBytes), nil } -// CalcDepositorFee calculates the depositor fee for a given block +// CalcDepositorFee calculates the depositor fee for a given tx result func CalcDepositorFee( - blockVb *btcjson.GetBlockVerboseTxResult, - chainID int64, - netParams *chaincfg.Params, - logger zerolog.Logger, -) float64 { - // use default fee for regnet - if chains.IsBitcoinRegnet(chainID) { - return DefaultDepositorFee - } - // mainnet dynamic fee takes effect only after a planned upgrade height - if chains.IsBitcoinMainnet(chainID) && blockVb.Height < DynamicDepositorFeeHeight { - return DefaultDepositorFee - } - - // calculate deposit fee rate - feeRate, err := CalcBlockAvgFeeRate(blockVb, netParams) - if err != nil { - 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 G115 always in range - feeRate = int64(float64(feeRate) * clientcommon.BTCOutboundGasPriceMultiplier) - - return DepositorFee(feeRate) -} - -// CalcDepositorFeeV2 calculates the depositor fee for a given tx result -func CalcDepositorFeeV2( rpcClient interfaces.BTCRPCClient, rawResult *btcjson.TxRawResult, netParams *chaincfg.Params, diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 5985d6f574..c65626da23 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -26,6 +26,30 @@ import ( "github.com/zeta-chain/node/zetaclient/zetacore" ) +// BTCInboundEvent represents an incoming transaction event +type BTCInboundEvent struct { + // FromAddress is the first input address + FromAddress string + + // ToAddress is the TSS address + ToAddress string + + // Value is the amount of BTC + Value float64 + + // DepositorFee is the deposit fee + DepositorFee float64 + + // MemoBytes is the memo of inbound + MemoBytes []byte + + // BlockNumber is the block number of the inbound + BlockNumber uint64 + + // TxHash is the hash of the inbound + TxHash string +} + // WatchInbound watches Bitcoin chain for inbounds on a ticker // It starts a ticker and run ObserveInbound // TODO(revamp): move all ticker related methods in the same file @@ -94,7 +118,6 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // #nosec G115 checked positive lastBlock := uint64(currentBlock) - if lastBlock < ob.LastBlock() { return fmt.Errorf( "observeInboundBTC: block number should not decrease: current %d last %d", @@ -104,45 +127,35 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { } ob.WithLastBlock(lastBlock) - // skip if current height is too low - if lastBlock < ob.GetChainParams().ConfirmationCount { - return fmt.Errorf("observeInboundBTC: skipping observer, current block number %d is too low", currentBlock) - } - - // skip if no new block is confirmed - lastScanned := ob.LastBlockScanned() - if lastScanned >= lastBlock-ob.GetChainParams().ConfirmationCount { + // check confirmation + blockNumber := ob.LastBlockScanned() + 1 + if !ob.IsBlockConfirmed(blockNumber) { return nil } // query incoming gas asset to TSS address - blockNumber := lastScanned + 1 // #nosec G115 always in range res, err := ob.GetBlockByNumberCached(int64(blockNumber)) if err != nil { ob.logger.Inbound.Error().Err(err).Msgf("observeInboundBTC: error getting bitcoin block %d", blockNumber) return err } - ob.logger.Inbound.Info().Msgf("observeInboundBTC: block %d has %d txs, current block %d, last block %d", - blockNumber, len(res.Block.Tx), currentBlock, lastScanned) + ob.logger.Inbound.Info().Msgf("observeInboundBTC: block %d has %d txs, current block %d, last scanned %d", + blockNumber, len(res.Block.Tx), currentBlock, blockNumber-1) // add block header to zetacore if len(res.Block.Tx) > 1 { - // get depositor fee - depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) - // filter incoming txs to TSS address tssAddress := ob.TSS().BTCAddress() // #nosec G115 always positive - inbounds, err := FilterAndParseIncomingTx( + events, err := FilterAndParseIncomingTx( ob.btcClient, res.Block.Tx, uint64(res.Block.Height), tssAddress, ob.logger.Inbound, ob.netParams, - depositorFee, ) if err != nil { ob.logger.Inbound.Error(). @@ -152,8 +165,8 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { } // post inbound vote message to zetacore - for _, inbound := range inbounds { - msg := ob.GetInboundVoteMessageFromBtcEvent(inbound) + for _, event := range events { + msg := ob.GetInboundVoteMessageFromBtcEvent(event) if msg != nil { zetaHash, ballot, err := zetaCoreClient.PostVoteInbound( ctx, @@ -164,11 +177,11 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { if err != nil { ob.logger.Inbound.Error(). Err(err). - Msgf("observeInboundBTC: error posting to zetacore for tx %s", inbound.TxHash) + Msgf("observeInboundBTC: error posting to zetacore for tx %s", event.TxHash) return err // we have to re-scan this block next time } else if zetaHash != "" { ob.logger.Inbound.Info().Msgf("observeInboundBTC: PostVoteInbound zeta tx hash: %s inbound %s ballot %s fee %v", - zetaHash, inbound.TxHash, ballot, depositorFee) + zetaHash, event.TxHash, ballot, event.DepositorFee) } } } @@ -272,12 +285,22 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, return "", fmt.Errorf("block %d has no transactions", blockVb.Height) } - depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) tss, err := ob.ZetacoreClient().GetBTCTSSAddress(ctx, ob.Chain().ChainId) if err != nil { return "", err } + // check confirmation + if !ob.IsBlockConfirmed(uint64(blockVb.Height)) { + return "", fmt.Errorf("block %d is not confirmed yet", blockVb.Height) + } + + // calculate depositor fee + depositorFee, err := bitcoin.CalcDepositorFee(ob.btcClient, tx, ob.netParams) + if err != nil { + return "", errors.Wrapf(err, "error calculating depositor fee for inbound %s", tx.Txid) + } + // #nosec G115 always positive event, err := GetBtcEvent( ob.btcClient, @@ -316,7 +339,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, return "", err } else if zetaHash != "" { ob.logger.Inbound.Info().Msgf("BTC deposit detected and reported: PostVoteInbound zeta tx hash: %s inbound %s ballot %s fee %v", - zetaHash, txHash, ballot, depositorFee) + zetaHash, txHash, ballot, event.DepositorFee) } return msg.Digest(), nil @@ -333,26 +356,31 @@ func FilterAndParseIncomingTx( tssAddress string, logger zerolog.Logger, netParams *chaincfg.Params, - depositorFee float64, ) ([]*BTCInboundEvent, error) { - inbounds := make([]*BTCInboundEvent, 0) + events := make([]*BTCInboundEvent, 0) for idx, tx := range txs { if idx == 0 { continue // the first tx is coinbase; we do not process coinbase tx } - inbound, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, depositorFee) + // calculate depositor fee + depositorFee, err := bitcoin.CalcDepositorFee(rpcClient, &txs[idx], netParams) + if err != nil { + return nil, errors.Wrapf(err, "error calculating depositor fee for inbound %s", tx.Txid) + } + + event, 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 inbound != nil { - inbounds = append(inbounds, inbound) + if event != nil { + events = append(events, event) logger.Info().Msgf("FilterAndParseIncomingTx: found btc event for tx %s in block %d", tx.Txid, blockNumber) } } - return inbounds, nil + return events, nil } // GetInboundVoteMessageFromBtcEvent converts a BTCInboundEvent to a MsgVoteInbound to enable voting on the inbound on zetacore @@ -434,19 +462,6 @@ func GetBtcEvent( return nil, nil } - // switch to depositor fee V2 if - // 1. it is bitcoin testnet, or - // 2. it is bitcoin mainnet and upgrade height is reached - // TODO: remove CalcDepositorFeeV1 and below conditions after the upgrade height - // https://github.com/zeta-chain/node/issues/2766 - if netParams.Name == chaincfg.TestNet3Params.Name || - (netParams.Name == chaincfg.MainNetParams.Name && blockNumber >= bitcoin.DynamicDepositorFeeHeightV2) { - depositorFee, err = bitcoin.CalcDepositorFeeV2(rpcClient, &tx, netParams) - if err != nil { - return nil, errors.Wrapf(err, "error calculating depositor fee V2 for inbound: %s", tx.Txid) - } - } - // deposit amount has to be no less than the minimum depositor fee if vout0.Value < depositorFee { logger.Info(). @@ -470,19 +485,52 @@ func GetBtcEvent( return nil, fmt.Errorf("GetBtcEvent: no input found for inbound: %s", tx.Txid) } + // get sender address by input (vin) fromAddress, err := GetSenderAddressByVin(rpcClient, tx.Vin[0], netParams) if err != nil { return nil, errors.Wrapf(err, "error getting sender address for inbound: %s", tx.Txid) } + // skip this tx and move on (e.g., due to unknown script type) + // we don't know whom to refund if this tx gets reverted in zetacore + if fromAddress == "" { + return nil, nil + } + return &BTCInboundEvent{ - FromAddress: fromAddress, - ToAddress: tssAddress, - Value: value, - MemoBytes: memo, - BlockNumber: blockNumber, - TxHash: tx.Txid, + FromAddress: fromAddress, + ToAddress: tssAddress, + Value: value, + DepositorFee: depositorFee, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, }, nil } return nil, nil } + +// GetSenderAddressByVin get the sender address from the transaction input (vin) +func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, net *chaincfg.Params) (string, error) { + // query previous raw transaction by txid + hash, err := chainhash.NewHashFromStr(vin.Txid) + if err != nil { + return "", err + } + + // this requires running bitcoin node with 'txindex=1' + tx, err := rpcClient.GetRawTransaction(hash) + if err != nil { + return "", errors.Wrapf(err, "error getting raw transaction %s", vin.Txid) + } + + // #nosec G115 - 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) + } + + // decode sender address from previous pkScript + pkScript := tx.MsgTx().TxOut[vin.Vout].PkScript + + return bitcoin.DecodeSenderFromScript(pkScript, net) +} diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index 13d975caab..2b7a333501 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -12,8 +12,9 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/wire" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" @@ -22,24 +23,9 @@ import ( clientcommon "github.com/zeta-chain/node/zetaclient/common" "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "github.com/zeta-chain/node/zetaclient/testutils/testrpc" ) -// createRPCClientAndLoadTx is a helper function to load raw tx and feed it to mock rpc client -func createRPCClientAndLoadTx(t *testing.T, chainId int64, txHash string) *mocks.MockBTCRPCClient { - // file name for the archived MsgTx - nameMsgTx := path.Join(TestDataDir, testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chainId, txHash)) - - // load archived MsgTx - var msgTx wire.MsgTx - testutils.LoadObjectFromJSONFile(t, &msgTx, nameMsgTx) - tx := btcutil.NewTx(&msgTx) - - // feed tx to mock rpc client - rpcClient := mocks.NewMockBTCRPCClient() - rpcClient.WithRawTransaction(tx) - return rpcClient -} - func TestAvgFeeRateBlock828440(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult @@ -152,74 +138,17 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { }) } -func TestCalcDepositorFee828440(t *testing.T) { - // load archived block 828440 - var blockVb btcjson.GetBlockVerboseTxResult - testutils.LoadObjectFromJSONFile( - t, - &blockVb, - path.Join(TestDataDir, testutils.TestDataPathBTC, "block_trimmed_8332_828440.json"), - ) - avgGasRate := float64(32.0) - // #nosec G115 test - always in range - - gasRate := int64(avgGasRate * clientcommon.BTCOutboundGasPriceMultiplier) - dynamicFee828440 := bitcoin.DepositorFee(gasRate) - - // should return default fee if it's a regtest block - fee := bitcoin.CalcDepositorFee(&blockVb, 18444, &chaincfg.RegressionNetParams, log.Logger) - require.Equal(t, bitcoin.DefaultDepositorFee, fee) - - // should return dynamic fee if it's a testnet block - fee = bitcoin.CalcDepositorFee(&blockVb, 18332, &chaincfg.TestNet3Params, log.Logger) - require.NotEqual(t, bitcoin.DefaultDepositorFee, fee) - require.Equal(t, dynamicFee828440, fee) - - // mainnet should return default fee before upgrade height - blockVb.Height = bitcoin.DynamicDepositorFeeHeight - 1 - fee = bitcoin.CalcDepositorFee(&blockVb, 8332, &chaincfg.MainNetParams, log.Logger) - require.Equal(t, bitcoin.DefaultDepositorFee, fee) - - // mainnet should return dynamic fee after upgrade height - blockVb.Height = bitcoin.DynamicDepositorFeeHeight - fee = bitcoin.CalcDepositorFee(&blockVb, 8332, &chaincfg.MainNetParams, log.Logger) - require.NotEqual(t, bitcoin.DefaultDepositorFee, fee) - require.Equal(t, dynamicFee828440, fee) -} - func TestGetSenderAddressByVin(t *testing.T) { + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" chain := chains.BitcoinMainnet net := &chaincfg.MainNetParams - t.Run("should get sender address from P2TR tx", func(t *testing.T) { - // vin from the archived P2TR tx - // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 - txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) - - // get sender address - txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) - require.NoError(t, err) - require.Equal(t, "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", sender) - }) - t.Run("should get sender address from P2WSH tx", func(t *testing.T) { - // vin from the archived P2WSH tx - // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 - txHash := "d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016" - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) - - // get sender address - txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) - require.NoError(t, err) - require.Equal(t, "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", sender) - }) - t.Run("should get sender address from P2WPKH tx", func(t *testing.T) { + t.Run("should get sender address from tx", func(t *testing.T) { // vin from the archived P2WPKH tx // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 txHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, txHash) // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} @@ -227,83 +156,31 @@ func TestGetSenderAddressByVin(t *testing.T) { require.NoError(t, err) require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) }) - t.Run("should get sender address from P2SH tx", func(t *testing.T) { - // vin from the archived P2SH tx - // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a - txHash := "211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a" - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) - - // get sender address - txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) - require.NoError(t, err) - require.Equal(t, "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", sender) - }) - t.Run("should get sender address from P2PKH tx", func(t *testing.T) { - // vin from the archived P2PKH tx - // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 - txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) - // get sender address - txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) - require.NoError(t, err) - require.Equal(t, "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", sender) - }) - t.Run("should get empty sender address on unknown script", func(t *testing.T) { - // vin from the archived P2PKH tx - // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 - txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" - nameMsgTx := path.Join( - TestDataDir, - testutils.TestDataPathBTC, - testutils.FileNameBTCMsgTx(chain.ChainId, txHash), - ) - var msgTx wire.MsgTx - testutils.LoadObjectFromJSONFile(t, &msgTx, nameMsgTx) - - // modify script to unknown script - msgTx.TxOut[1].PkScript = []byte{0x00, 0x01, 0x02, 0x03} // can be any invalid script bytes - tx := btcutil.NewTx(&msgTx) - - // feed tx to mock rpc client - rpcClient := mocks.NewMockBTCRPCClient() - rpcClient.WithRawTransaction(tx) - - // get sender address - txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) - require.NoError(t, err) - require.Empty(t, sender) - }) -} - -func TestGetSenderAddressByVinErrors(t *testing.T) { - // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 - txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" - chain := chains.BitcoinMainnet - net := &chaincfg.MainNetParams - - t.Run("should get sender address from P2TR tx", func(t *testing.T) { - rpcClient := mocks.NewMockBTCRPCClient() + t.Run("should return error on invalid txHash", func(t *testing.T) { + rpcClient := mocks.NewBTCRPCClient(t) // use invalid tx hash txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.Error(t, err) require.Empty(t, sender) }) + t.Run("should return error when RPC client fails to get raw tx", func(t *testing.T) { - // create mock rpc client without preloaded tx - rpcClient := mocks.NewMockBTCRPCClient() + // create mock rpc client that returns rpc error + rpcClient := mocks.NewBTCRPCClient(t) + rpcClient.On("GetRawTransaction", mock.Anything).Return(nil, errors.New("rpc error")) + + // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "error getting raw transaction") require.Empty(t, sender) }) + t.Run("should return error on invalid output index", func(t *testing.T) { // create mock rpc client with preloaded tx - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, txHash) // invalid output index txVin := btcjson.Vin{Txid: txHash, Vout: 3} sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) @@ -323,19 +200,21 @@ func TestGetBtcEvent(t *testing.T) { tssAddress := testutils.TSSAddressBTCMainnet blockNumber := uint64(835640) net := &chaincfg.MainNetParams - // 2.992e-05, see avgFeeRate https://mempool.space/api/v1/blocks/835640 - depositorFee := bitcoin.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier) + + // fee rate of above tx is 28 sat/vB + depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) // expected result memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) require.NoError(t, err) eventExpected := &observer.BTCInboundEvent{ - FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", - ToAddress: tssAddress, - Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis - MemoBytes: memo, - BlockNumber: blockNumber, - TxHash: tx.Txid, + FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, // 6192 sataoshis + DepositorFee: depositorFee, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, } t.Run("should get BTC inbound event from P2WPKH sender", func(t *testing.T) { @@ -345,13 +224,14 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 2 eventExpected.FromAddress = "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) + t.Run("should get BTC inbound event from P2TR sender", func(t *testing.T) { // replace vin with a P2TR vin, so the sender address will change // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 @@ -360,13 +240,14 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 2 eventExpected.FromAddress = "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) + t.Run("should get BTC inbound event from P2WSH sender", func(t *testing.T) { // replace vin with a P2WSH vin, so the sender address will change // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 @@ -375,13 +256,14 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 0 eventExpected.FromAddress = "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) + t.Run("should get BTC inbound event from P2SH sender", func(t *testing.T) { // replace vin with a P2SH vin, so the sender address will change // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a @@ -390,13 +272,14 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 0 eventExpected.FromAddress = "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) + t.Run("should get BTC inbound event from P2PKH sender", func(t *testing.T) { // replace vin with a P2PKH vin, so the sender address will change // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 @@ -405,27 +288,29 @@ func TestGetBtcEvent(t *testing.T) { tx.Vin[0].Vout = 1 eventExpected.FromAddress = "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV" // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) + t.Run("should skip tx if len(tx.Vout) < 2", func(t *testing.T) { // load tx and modify the tx to have only 1 vout tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) tx.Vout = tx.Vout[:1] // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) + t.Run("should skip tx if Vout[0] is not a P2WPKH output", func(t *testing.T) { // load tx - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) // modify the tx to have Vout[0] a P2SH output @@ -440,46 +325,73 @@ func TestGetBtcEvent(t *testing.T) { require.NoError(t, err) require.Nil(t, event) }) + t.Run("should skip tx if receiver address is not TSS address", func(t *testing.T) { // load tx and modify receiver address to any non-tss address: bc1qw8wrek2m7nlqldll66ajnwr9mh64syvkt67zlu tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) + t.Run("should skip tx if amount is less than depositor fee", func(t *testing.T) { // load tx and modify amount to less than depositor fee tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) + t.Run("should skip tx if 2nd vout is not OP_RETURN", func(t *testing.T) { // load tx and modify memo OP_RETURN to OP_1 tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a", "51", 1) // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) + t.Run("should skip tx if memo decoding fails", func(t *testing.T) { // load tx and modify memo length to be 1 byte less than actual tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a14", "6a13", 1) // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + + t.Run("should skip tx if sender address is empty", func(t *testing.T) { + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + preVout := uint32(2) + preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = preVout + + // create mock rpc client + rpcClient := mocks.NewBTCRPCClient(t) + + // load archived MsgTx and modify previous input script to invalid + msgTx := testutils.LoadBTCMsgTx(t, TestDataDir, chain.ChainId, preHash) + msgTx.TxOut[preVout].PkScript = []byte{0x00, 0x01} + + // mock rpc response to return invalid tx msg + rpcClient.On("GetRawTransaction", mock.Anything).Return(btcutil.NewTx(msgTx), nil) + + // get BTC event event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) @@ -494,7 +406,9 @@ func TestGetBtcEventErrors(t *testing.T) { net := &chaincfg.MainNetParams tssAddress := testutils.TSSAddressBTCMainnet blockNumber := uint64(835640) - depositorFee := bitcoin.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier) + + // fee rate of above tx is 28 sat/vB + depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) t.Run("should return error on invalid Vout[0] script", func(t *testing.T) { // load tx and modify Vout[0] script to invalid script @@ -502,30 +416,35 @@ func TestGetBtcEventErrors(t *testing.T) { tx.Vout[0].ScriptPubKey.Hex = "0014invalid000000000000000000000000000000000" // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) + t.Run("should return error if len(tx.Vin) < 1", func(t *testing.T) { // load tx and remove vin tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) tx.Vin = nil // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) - require.Error(t, err) + require.ErrorContains(t, err, "no input found") require.Nil(t, event) }) + t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { // load tx and leave rpc client without preloaded tx tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) - rpcClient := mocks.NewMockBTCRPCClient() + + // create mock rpc client that returns rpc error + rpcClient := mocks.NewBTCRPCClient(t) + rpcClient.On("GetRawTransaction", mock.Anything).Return(nil, errors.New("rpc error")) // get BTC event event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) - require.Error(t, err) + require.ErrorContains(t, err, "error getting sender address") require.Nil(t, event) }) } diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index bf13863f68..a699767b4c 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -3,7 +3,6 @@ package observer import ( "context" - "encoding/hex" "fmt" "math" "math/big" @@ -13,7 +12,6 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -54,23 +52,6 @@ type Logger struct { UTXOs zerolog.Logger } -// BTCInboundEvent represents an incoming transaction event -// TODO(revamp): Move to inbound -type BTCInboundEvent 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 -} - // BTCBlockNHeader contains bitcoin block and the header type BTCBlockNHeader struct { Header *wire.BlockHeader @@ -332,49 +313,6 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { return nil } -// GetSenderAddressByVin get the sender address from the previous transaction -// TODO(revamp): move in upper package to separate file (e.g., rpc.go) -func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, net *chaincfg.Params) (string, error) { - // query previous raw transaction by txid - hash, err := chainhash.NewHashFromStr(vin.Txid) - if err != nil { - return "", err - } - - // this requires running bitcoin node with 'txindex=1' - tx, err := rpcClient.GetRawTransaction(hash) - if err != nil { - return "", errors.Wrapf(err, "error getting raw transaction %s", vin.Txid) - } - - // #nosec G115 - 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) - } - - // decode sender address from previous pkScript - pkScript := tx.MsgTx().TxOut[vin.Vout].PkScript - scriptHex := hex.EncodeToString(pkScript) - if bitcoin.IsPkScriptP2TR(pkScript) { - return bitcoin.DecodeScriptP2TR(scriptHex, net) - } - if bitcoin.IsPkScriptP2WSH(pkScript) { - return bitcoin.DecodeScriptP2WSH(scriptHex, net) - } - if bitcoin.IsPkScriptP2WPKH(pkScript) { - return bitcoin.DecodeScriptP2WPKH(scriptHex, net) - } - if bitcoin.IsPkScriptP2SH(pkScript) { - return bitcoin.DecodeScriptP2SH(scriptHex, net) - } - if bitcoin.IsPkScriptP2PKH(pkScript) { - return bitcoin.DecodeScriptP2PKH(scriptHex, net) - } - - // sender address not found, return nil and move on to the next tx - return "", nil -} - // WatchUTXOs watches bitcoin chain for UTXOs owned by the TSS address // TODO(revamp): move ticker related functions to a specific file func (ob *Observer) WatchUTXOs(ctx context.Context) error { diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 60686ff568..47172f6141 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -1,7 +1,6 @@ package observer_test import ( - "fmt" "math/big" "os" "strconv" @@ -10,6 +9,8 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/wire" lru "github.com/hashicorp/golang-lru" + "github.com/pkg/errors" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/zetaclient/db" "gorm.io/gorm" @@ -71,10 +72,13 @@ func MockBTCObserver( ) *observer.Observer { // use default mock btc client if not provided if btcClient == nil { - btcClient = mocks.NewMockBTCRPCClient().WithBlockCount(100) + rpcClient := mocks.NewBTCRPCClient(t) + rpcClient.On("GetBlockCount").Return(int64(100), nil) + btcClient = rpcClient } database, err := db.NewFromSqliteInMemory(true) + require.NoError(t, err) // create observer ob, err := observer.NewObserver( @@ -98,6 +102,10 @@ func Test_NewObserver(t *testing.T) { chain := chains.BitcoinMainnet params := mocks.MockChainParams(chain.ChainId, 10) + // create mock btc client with block height 100 + btcClient := mocks.NewBTCRPCClient(t) + btcClient.On("GetBlockCount").Return(int64(100), nil) + // test cases tests := []struct { name string @@ -115,7 +123,7 @@ func Test_NewObserver(t *testing.T) { { name: "should be able to create observer", chain: chain, - btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + btcClient: btcClient, chainParams: params, coreClient: nil, tss: mocks.NewTSSMainnet(), @@ -123,7 +131,7 @@ func Test_NewObserver(t *testing.T) { { name: "should fail if net params is not found", chain: chains.Chain{ChainId: 111}, // invalid chain id - btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + btcClient: btcClient, chainParams: params, coreClient: nil, tss: mocks.NewTSSMainnet(), @@ -132,7 +140,7 @@ func Test_NewObserver(t *testing.T) { { name: "should fail if env var us invalid", chain: chain, - btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + btcClient: btcClient, chainParams: params, coreClient: nil, tss: mocks.NewTSSMainnet(), @@ -197,16 +205,16 @@ func Test_BlockCache(t *testing.T) { ob.WithBlockCache(blockCache) // create mock btc client - btcClient := mocks.NewMockBTCRPCClient() + btcClient := mocks.NewBTCRPCClient(t) ob.WithBtcClient(btcClient) // feed block hash, header and block to btc client hash := sample.BtcHash() header := &wire.BlockHeader{Version: 1} block := &btcjson.GetBlockVerboseTxResult{Version: 1} - btcClient.WithBlockHash(&hash) - btcClient.WithBlockHeader(header) - btcClient.WithBlockVerboseTx(block) + btcClient.On("GetBlockHash", mock.Anything).Return(&hash, nil) + btcClient.On("GetBlockHeader", &hash).Return(header, nil) + btcClient.On("GetBlockVerboseTx", &hash).Return(block, nil) // get block and header from observer, fallback to btc client result, err := ob.GetBlockByNumberCached(100) @@ -243,8 +251,9 @@ func Test_LoadLastBlockScanned(t *testing.T) { chain := chains.BitcoinMainnet params := mocks.MockChainParams(chain.ChainId, 10) - // create observer using mock btc client - btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(200) + // create mock btc client with block height 200 + btcClient := mocks.NewBTCRPCClient(t) + btcClient.On("GetBlockCount").Return(int64(200), nil) t.Run("should load last block scanned", func(t *testing.T) { // create observer and write 199 as last block scanned @@ -276,17 +285,19 @@ func Test_LoadLastBlockScanned(t *testing.T) { // reset last block scanned to 0 so that it will be loaded from RPC obOther.WithLastBlockScanned(0) - // set RPC error - btcClient.WithError(fmt.Errorf("error RPC")) + // attach a mock btc client that returns rpc error + errClient := mocks.NewBTCRPCClient(t) + errClient.On("GetBlockCount").Return(int64(0), errors.New("rpc error")) + obOther.WithBtcClient(errClient) // load last block scanned err := obOther.LoadLastBlockScanned() - require.ErrorContains(t, err, "error RPC") + require.ErrorContains(t, err, "rpc error") }) t.Run("should use hardcode block 100 for regtest", func(t *testing.T) { // use regtest chain regtest := chains.BitcoinRegtest - obRegnet := MockBTCObserver(t, regtest, params, nil) + obRegnet := MockBTCObserver(t, regtest, params, btcClient) // load last block scanned err := obRegnet.LoadLastBlockScanned() diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index 3cad907a19..ac9fb2609a 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -24,10 +24,13 @@ var TestDataDir = "../../../" func MockBTCObserverMainnet(t *testing.T) *Observer { // setup mock arguments chain := chains.BitcoinMainnet - btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) params := mocks.MockChainParams(chain.ChainId, 10) tss := mocks.NewTSSMainnet() + // create mock rpc client + btcClient := mocks.NewBTCRPCClient(t) + btcClient.On("GetBlockCount").Return(int64(100), nil) + database, err := db.NewFromSqliteInMemory(true) require.NoError(t, err) diff --git a/zetaclient/chains/bitcoin/observer/witness.go b/zetaclient/chains/bitcoin/observer/witness.go index 142e6e796a..696629b59a 100644 --- a/zetaclient/chains/bitcoin/observer/witness.go +++ b/zetaclient/chains/bitcoin/observer/witness.go @@ -67,13 +67,20 @@ func GetBtcEventWithWitness( return nil, errors.Wrapf(err, "error getting sender address for inbound: %s", tx.Txid) } + // skip this tx and move on (e.g., due to unknown script type) + // we don't know whom to refund if this tx gets reverted in zetacore + if fromAddress == "" { + return nil, nil + } + return &BTCInboundEvent{ - FromAddress: fromAddress, - ToAddress: tssAddress, - Value: amount, - MemoBytes: memo, - BlockNumber: blockNumber, - TxHash: tx.Txid, + FromAddress: fromAddress, + ToAddress: tssAddress, + Value: amount, + DepositorFee: depositorFee, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, }, nil } diff --git a/zetaclient/chains/bitcoin/observer/witness_test.go b/zetaclient/chains/bitcoin/observer/witness_test.go index e85ffa9912..af14427c6e 100644 --- a/zetaclient/chains/bitcoin/observer/witness_test.go +++ b/zetaclient/chains/bitcoin/observer/witness_test.go @@ -4,8 +4,11 @@ import ( "encoding/hex" "testing" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" @@ -14,6 +17,7 @@ import ( clientcommon "github.com/zeta-chain/node/zetaclient/common" "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "github.com/zeta-chain/node/zetaclient/testutils/testrpc" ) func TestParseScriptFromWitness(t *testing.T) { @@ -45,7 +49,7 @@ func TestParseScriptFromWitness(t *testing.T) { }) } -func TestGetBtcEventFromInscription(t *testing.T) { +func TestGetBtcEventWithWitness(t *testing.T) { // load archived inbound P2WPKH raw result // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" @@ -54,12 +58,14 @@ func TestGetBtcEventFromInscription(t *testing.T) { tssAddress := testutils.TSSAddressBTCMainnet blockNumber := uint64(835640) net := &chaincfg.MainNetParams - // 2.992e-05, see avgFeeRate https://mempool.space/api/v1/blocks/835640 - depositorFee := bitcoin.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier) + + // fee rate of above tx is 28 sat/vB + depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) t.Run("decode OP_RETURN ok", func(t *testing.T) { tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) + // mock up the input // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" tx.Vin[0].Txid = preHash @@ -67,16 +73,17 @@ func TestGetBtcEventFromInscription(t *testing.T) { memo, _ := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) eventExpected := &observer.BTCInboundEvent{ - FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", - ToAddress: tssAddress, - Value: tx.Vout[0].Value - depositorFee, - MemoBytes: memo, - BlockNumber: blockNumber, - TxHash: tx.Txid, + FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, + DepositorFee: depositorFee, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, } // load previous raw tx so so mock rpc client can return it - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event event, err := observer.GetBtcEventWithWitness( @@ -96,16 +103,24 @@ func TestGetBtcEventFromInscription(t *testing.T) { txHash2 := "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8" tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false) + // mock up the input + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 2 + + // load previous raw tx so so mock rpc client can return it + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) + // get BTC event eventExpected := &observer.BTCInboundEvent{ - FromAddress: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", - ToAddress: tssAddress, - Value: tx.Vout[0].Value - depositorFee, - MemoBytes: make([]byte, 600), - BlockNumber: blockNumber, - TxHash: tx.Txid, + FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, + DepositorFee: depositorFee, + MemoBytes: make([]byte, 600), + BlockNumber: blockNumber, + TxHash: tx.Txid, } // get BTC event @@ -119,7 +134,7 @@ func TestGetBtcEventFromInscription(t *testing.T) { depositorFee, ) require.NoError(t, err) - require.Equal(t, event, eventExpected) + require.Equal(t, eventExpected, event) }) t.Run("decode inscription ok - mainnet", func(t *testing.T) { @@ -127,21 +142,26 @@ func TestGetBtcEventFromInscription(t *testing.T) { txHash2 := "7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c" tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false) + // mock up the input + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" tx.Vin[0].Txid = preHash - tx.Vin[0].Sequence = 2 - rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + tx.Vin[0].Vout = 2 + + // load previous raw tx so so mock rpc client can return it + rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) memo, _ := hex.DecodeString( "72f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c", ) eventExpected := &observer.BTCInboundEvent{ - FromAddress: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", - ToAddress: tssAddress, - Value: tx.Vout[0].Value - depositorFee, - MemoBytes: memo, - BlockNumber: blockNumber, - TxHash: tx.Txid, + FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, + DepositorFee: depositorFee, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, } // get BTC event @@ -164,7 +184,7 @@ func TestGetBtcEventFromInscription(t *testing.T) { tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEventWithWitness( rpcClient, *tx, @@ -184,7 +204,7 @@ func TestGetBtcEventFromInscription(t *testing.T) { tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee // get BTC event - rpcClient := mocks.NewMockBTCRPCClient() + rpcClient := mocks.NewBTCRPCClient(t) event, err := observer.GetBtcEventWithWitness( rpcClient, *tx, @@ -199,9 +219,12 @@ func TestGetBtcEventFromInscription(t *testing.T) { }) t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { - // load tx and leave rpc client without preloaded tx + // load tx tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) - rpcClient := mocks.NewMockBTCRPCClient() + + // create mock rpc client that returns rpc error + rpcClient := mocks.NewBTCRPCClient(t) + rpcClient.On("GetRawTransaction", mock.Anything).Return(nil, errors.New("rpc error")) // get BTC event event, err := observer.GetBtcEventWithWitness( @@ -213,14 +236,29 @@ func TestGetBtcEventFromInscription(t *testing.T) { net, depositorFee, ) - require.Error(t, err) + require.ErrorContains(t, err, "rpc error") require.Nil(t, event) }) - t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { - // load tx and leave rpc client without preloaded tx + t.Run("should skip tx if sender address is empty", func(t *testing.T) { + // load tx tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) - rpcClient := mocks.NewMockBTCRPCClient() + + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + preVout := uint32(2) + preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = preVout + + // create mock rpc client + rpcClient := mocks.NewBTCRPCClient(t) + + // load archived MsgTx and modify previous input script to invalid + msgTx := testutils.LoadBTCMsgTx(t, TestDataDir, chain.ChainId, preHash) + msgTx.TxOut[preVout].PkScript = []byte{0x00, 0x01} + + // mock rpc response to return invalid tx msg + rpcClient.On("GetRawTransaction", mock.Anything).Return(btcutil.NewTx(msgTx), nil) // get BTC event event, err := observer.GetBtcEventWithWitness( @@ -232,7 +270,7 @@ func TestGetBtcEventFromInscription(t *testing.T) { net, depositorFee, ) - require.Error(t, err) + require.NoError(t, err) require.Nil(t, event) }) } diff --git a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go index 2eee4bf023..f3fdf5f12d 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go @@ -137,7 +137,6 @@ func LiveTest_FilterAndParseIncomingTx(t *testing.T) { "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", log.Logger, &chaincfg.TestNet3Params, - 0.0, ) require.NoError(t, err) require.Len(t, inbounds, 1) @@ -175,7 +174,6 @@ func LiveTest_FilterAndParseIncomingTx_Nop(t *testing.T) { "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", log.Logger, &chaincfg.TestNet3Params, - 0.0, ) require.NoError(t, err) @@ -583,7 +581,7 @@ func LiveTest_GetTransactionFeeAndRate(t *testing.T) { } } -func LiveTest_CalcDepositorFeeV2(t *testing.T) { +func LiveTest_CalcDepositorFee(t *testing.T) { // setup Bitcoin client client, err := createRPCClient(chains.BitcoinMainnet.ChainId) require.NoError(t, err) @@ -598,13 +596,13 @@ func LiveTest_CalcDepositorFeeV2(t *testing.T) { require.NoError(t, err) t.Run("should return default depositor fee", func(t *testing.T) { - depositorFee, err := bitcoin.CalcDepositorFeeV2(client, rawResult, &chaincfg.RegressionNetParams) + depositorFee, err := bitcoin.CalcDepositorFee(client, rawResult, &chaincfg.RegressionNetParams) require.NoError(t, err) require.Equal(t, bitcoin.DefaultDepositorFee, depositorFee) }) t.Run("should return correct depositor fee for a given tx", func(t *testing.T) { - depositorFee, err := bitcoin.CalcDepositorFeeV2(client, rawResult, &chaincfg.MainNetParams) + depositorFee, err := bitcoin.CalcDepositorFee(client, rawResult, &chaincfg.MainNetParams) require.NoError(t, err) // the actual fee rate is 860 sat/vByte diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 1cbd9ea52e..f5bc856d5d 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -233,6 +233,28 @@ func EncodeAddress(hash160 []byte, netID byte) string { return base58.CheckEncode(hash160[:ripemd160.Size], netID) } +// DecodeSenderFromScript decodes sender from a given script +func DecodeSenderFromScript(pkScript []byte, net *chaincfg.Params) (string, error) { + scriptHex := hex.EncodeToString(pkScript) + + // decode sender address from according to script type + switch { + case IsPkScriptP2TR(pkScript): + return DecodeScriptP2TR(scriptHex, net) + case IsPkScriptP2WSH(pkScript): + return DecodeScriptP2WSH(scriptHex, net) + case IsPkScriptP2WPKH(pkScript): + return DecodeScriptP2WPKH(scriptHex, net) + case IsPkScriptP2SH(pkScript): + return DecodeScriptP2SH(scriptHex, net) + case IsPkScriptP2PKH(pkScript): + return DecodeScriptP2PKH(scriptHex, net) + default: + // sender address not found, return nil and move on to the next tx + return "", nil + } +} + // DecodeTSSVout decodes receiver and amount from a given TSS vout func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chain) (string, int64, error) { // parse amount diff --git a/zetaclient/chains/bitcoin/tx_script_test.go b/zetaclient/chains/bitcoin/tx_script_test.go index 68910fd49f..cf54b4553f 100644 --- a/zetaclient/chains/bitcoin/tx_script_test.go +++ b/zetaclient/chains/bitcoin/tx_script_test.go @@ -1,4 +1,4 @@ -package bitcoin +package bitcoin_test import ( "encoding/hex" @@ -12,6 +12,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/testutils" ) @@ -29,7 +30,7 @@ func TestDecodeVoutP2TR(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2TR - receiver, err := DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := bitcoin.DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", receiver) } @@ -45,27 +46,30 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) + t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 - _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) + t.Run("should return error on invalid OP_1", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_1 '51' to OP_2 '52' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "51", "52", 1) - _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) + t.Run("should return error on wrong hash length", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '20' to '19' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "5120", "5119", 1) - _, err := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) } @@ -81,7 +85,7 @@ func TestDecodeVoutP2WSH(t *testing.T) { require.Len(t, rawResult.Vout, 1) // decode vout 0, P2WSH - receiver, err := DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := bitcoin.DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", receiver) } @@ -97,27 +101,30 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) + t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 - _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) + t.Run("should return error on invalid OP_0", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_0 '00' to OP_1 '51' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "00", "51", 1) - _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) + t.Run("should return error on wrong hash length", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '20' to '19' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0020", "0019", 1) - _, err := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) } @@ -135,17 +142,17 @@ func TestDecodeP2WPKHVout(t *testing.T) { require.Len(t, rawResult.Vout, 3) // decode vout 0, nonce mark 148 - receiver, err := DecodeScriptP2WPKH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := bitcoin.DecodeScriptP2WPKH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) // decode vout 1, payment 0.00012000 BTC - receiver, err = DecodeScriptP2WPKH(rawResult.Vout[1].ScriptPubKey.Hex, net) + receiver, err = bitcoin.DecodeScriptP2WPKH(rawResult.Vout[1].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qpsdlklfcmlcfgm77c43x65ddtrt7n0z57hsyjp", receiver) // decode vout 2, change 0.39041489 BTC - receiver, err = DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) + receiver, err = bitcoin.DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) } @@ -164,20 +171,22 @@ func TestDecodeP2WPKHVoutErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) + t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 22 - _, err := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WPKH script") }) + t.Run("should return error on wrong hash length", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0014", "0013", 1) - _, err := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WPKH script") }) } @@ -193,7 +202,7 @@ func TestDecodeVoutP2SH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2SH - receiver, err := DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := bitcoin.DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", receiver) } @@ -209,33 +218,36 @@ func TestDecodeVoutP2SHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) + t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 23 - _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_HASH160 'a9' to OP_HASH256 'aa' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a9", "aa", 1) - _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) + t.Run("should return error on wrong data length", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a914", "a913", 1) - _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) + t.Run("should return error on invalid OP_EQUAL", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "87", "88", 1) - _, err := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) } @@ -251,7 +263,7 @@ func TestDecodeVoutP2PKH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2PKH - receiver, err := DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := bitcoin.DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", receiver) } @@ -267,48 +279,53 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) + t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "76a914" // 3 bytes, should be 25 - _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) + t.Run("should return error on invalid OP_DUP", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_DUP '76' to OP_NIP '77' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76", "77", 1) - _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_HASH160 'a9' to OP_HASH256 'aa' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a9", "76aa", 1) - _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) + t.Run("should return error on wrong data length", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a914", "76a913", 1) - _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) + t.Run("should return error on invalid OP_EQUALVERIFY", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_EQUALVERIFY '88' to OP_RESERVED1 '89' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "89ac", 1) - _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) + t.Run("should return error on invalid OP_CHECKSIG", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_CHECKSIG 'ac' to OP_CHECKSIGVERIFY 'ad' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "88ad", 1) - _, err := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) } @@ -324,7 +341,7 @@ func TestDecodeOpReturnMemo(t *testing.T) { require.Equal(t, scriptHex, rawResult.Vout[1].ScriptPubKey.Hex) t.Run("should decode memo from OP_RETURN output", func(t *testing.T) { - memo, found, err := DecodeOpReturnMemo(rawResult.Vout[1].ScriptPubKey.Hex, txHash) + memo, found, err := bitcoin.DecodeOpReturnMemo(rawResult.Vout[1].ScriptPubKey.Hex, txHash) require.NoError(t, err) require.True(t, found) // [OP_RETURN, 0x14,<20-byte-hash>] @@ -333,7 +350,7 @@ func TestDecodeOpReturnMemo(t *testing.T) { t.Run("should return nil memo non-OP_RETURN output", func(t *testing.T) { // modify the OP_RETURN to OP_1 scriptInvalid := strings.Replace(scriptHex, "6a", "51", 1) - memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) require.NoError(t, err) require.False(t, found) require.Nil(t, memo) @@ -341,7 +358,7 @@ func TestDecodeOpReturnMemo(t *testing.T) { t.Run("should return nil memo on invalid script", func(t *testing.T) { // use known short script scriptInvalid := "00" - memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) require.NoError(t, err) require.False(t, found) require.Nil(t, memo) @@ -356,37 +373,119 @@ func TestDecodeOpReturnMemoErrors(t *testing.T) { t.Run("should return error on invalid memo size", func(t *testing.T) { // use invalid memo size scriptInvalid := strings.Replace(scriptHex, "6a14", "6axy", 1) - memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) require.ErrorContains(t, err, "error decoding memo size") require.False(t, found) require.Nil(t, memo) }) + t.Run("should return error on memo size mismatch", func(t *testing.T) { // use wrong memo size scriptInvalid := strings.Replace(scriptHex, "6a14", "6a13", 1) - memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) require.ErrorContains(t, err, "memo size mismatch") require.False(t, found) require.Nil(t, memo) }) + t.Run("should return error on invalid hex", func(t *testing.T) { // use invalid hex scriptInvalid := strings.Replace(scriptHex, "6a1467", "6a14xy", 1) - memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) require.ErrorContains(t, err, "error hex decoding memo") require.False(t, found) require.Nil(t, memo) }) + t.Run("should return nil memo on donation tx", func(t *testing.T) { // use donation sctipt "6a0a4920616d207269636821" scriptDonation := "6a0a" + hex.EncodeToString([]byte(constant.DonationMessage)) - memo, found, err := DecodeOpReturnMemo(scriptDonation, txHash) + memo, found, err := bitcoin.DecodeOpReturnMemo(scriptDonation, txHash) require.ErrorContains(t, err, "donation tx") require.False(t, found) require.Nil(t, memo) }) } +func TestDecodeSenderFromScript(t *testing.T) { + chain := chains.BitcoinMainnet + net := &chaincfg.MainNetParams + + // Define a table of test cases + tests := []struct { + name string + txHash string + outputIndex int + expectedSender string + invalidScript bool // use invalid script or not + }{ + { + name: "should decode sender address from P2TR tx", + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + txHash: "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867", + outputIndex: 2, + expectedSender: "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", + }, + { + name: "should decode sender address from P2WSH tx", + // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 + txHash: "d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016", + outputIndex: 0, + expectedSender: "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", + }, + { + name: "should decode sender address from P2WPKH tx", + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + txHash: "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697", + outputIndex: 2, + expectedSender: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + }, + { + name: "should decode sender address from P2SH tx", + // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a + txHash: "211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a", + outputIndex: 0, + expectedSender: "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", + }, + { + name: "should decode sender address from P2PKH tx", + // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 + txHash: "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7", + outputIndex: 1, + expectedSender: "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", + }, + { + name: "should decode empty sender address on unknown script", + expectedSender: "", + invalidScript: true, // use invalid tx script + }, + } + + // Run through the test cases + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var err error + var pkScript []byte + + // Load the archived tx or invalid script + if tt.invalidScript { + // Use invalid script for the unknown script test case + pkScript = []byte{0x00, 0x01, 0x02, 0x03} + } else { + msgTx := testutils.LoadBTCMsgTx(t, TestDataDir, chain.ChainId, tt.txHash) + pkScript = msgTx.TxOut[tt.outputIndex].PkScript + } + + // Decode the sender address from the script + sender, err := bitcoin.DecodeSenderFromScript(pkScript, net) + + // Validate the results + require.NoError(t, err) + require.Equal(t, tt.expectedSender, sender) + }) + } +} + func TestDecodeTSSVout(t *testing.T) { chain := chains.BitcoinMainnet @@ -396,51 +495,55 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2TR", txHash) receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" - receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(45000), amount) }) + t.Run("should decode P2WSH vout", func(t *testing.T) { // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2WSH", txHash) receiverExpected := "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc" - receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(36557203), amount) }) + t.Run("should decode P2WPKH vout", func(t *testing.T) { // https://mempool.space/tx/5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b txHash := "5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b" rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2WPKH", txHash) receiverExpected := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" - receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(79938), amount) }) + t.Run("should decode P2SH vout", func(t *testing.T) { // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2SH", txHash) receiverExpected := "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE" - receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(1003881), amount) }) + t.Run("should decode P2PKH vout", func(t *testing.T) { // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2PKH", txHash) receiverExpected := "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte" - receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(1140000), amount) @@ -459,33 +562,40 @@ func TestDecodeTSSVoutErrors(t *testing.T) { t.Run("should return error on invalid amount", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.Value = -0.05 // use negative amount - receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, chain) + receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, chain) require.ErrorContains(t, err, "error getting satoshis") require.Empty(t, receiver) require.Zero(t, amount) }) + t.Run("should return error on invalid btc chain", func(t *testing.T) { invalidVout := rawResult.Vout[0] // use invalid chain invalidChain := chains.Chain{ChainId: 123} - receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, invalidChain) + receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, invalidChain) require.ErrorContains(t, err, "error GetBTCChainParams") require.Empty(t, receiver) require.Zero(t, amount) }) + t.Run("should return error when invalid receiver passed", func(t *testing.T) { invalidVout := rawResult.Vout[0] // use testnet params to decode mainnet receiver wrongChain := chains.BitcoinTestnet - receiver, amount, err := DecodeTSSVout(invalidVout, "bc1qulmx8ej27cj0xe20953cztr2excnmsqvuh0s5c", wrongChain) + receiver, amount, err := bitcoin.DecodeTSSVout( + invalidVout, + "bc1qulmx8ej27cj0xe20953cztr2excnmsqvuh0s5c", + wrongChain, + ) require.ErrorContains(t, err, "error decoding receiver") require.Empty(t, receiver) require.Zero(t, amount) }) + t.Run("should return error on decoding failure", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, chain) + receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, chain) require.ErrorContains(t, err, "error decoding TSS vout") require.Empty(t, receiver) require.Zero(t, amount) @@ -498,7 +608,7 @@ func TestDecodeScript(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ac00634d0802c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f4c50646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe68" script, _ := hex.DecodeString(data) - memo, isFound, err := DecodeScript(script) + memo, isFound, err := bitcoin.DecodeScript(script) require.Nil(t, err) require.True(t, isFound) @@ -512,7 +622,7 @@ func TestDecodeScript(t *testing.T) { data := "20d6f59371037bf30115d9fd6016f0e3ef552cdfc0367ee20aa9df3158f74aaeb4ac00634c51bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c397160881668" script, _ := hex.DecodeString(data) - memo, isFound, err := DecodeScript(script) + memo, isFound, err := bitcoin.DecodeScript(script) require.Nil(t, err) require.True(t, isFound) @@ -526,7 +636,7 @@ func TestDecodeScript(t *testing.T) { data := "20cabd6ecc0245c40f27ca6299dcd3732287c317f3946734f04e27568fc5334218ac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068" script, _ := hex.DecodeString(data) - memo, isFound, err := DecodeScript(script) + memo, isFound, err := bitcoin.DecodeScript(script) require.ErrorContains(t, err, "should contain more data, but script ended") require.False(t, isFound) require.Nil(t, memo) @@ -537,7 +647,7 @@ func TestDecodeScript(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" script, _ := hex.DecodeString(data) - memo, isFound, err := DecodeScript(script) + memo, isFound, err := bitcoin.DecodeScript(script) require.ErrorContains(t, err, "cannot obtain public key bytes") require.False(t, isFound) require.Nil(t, memo) @@ -548,7 +658,7 @@ func TestDecodeScript(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ab" script, _ := hex.DecodeString(data) - memo, isFound, err := DecodeScript(script) + memo, isFound, err := bitcoin.DecodeScript(script) require.ErrorContains(t, err, "cannot parse OP_CHECKSIG") require.False(t, isFound) require.Nil(t, memo) diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json b/zetaclient/testdata/btc/chain_8332_msgtx_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json new file mode 100644 index 0000000000..fd588d5335 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json @@ -0,0 +1,27 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 151, 38, 101, 150, 130, 143, 142, 0, 25, 145, 204, 136, 26, 72, 94, + 178, 23, 42, 52, 194, 81, 114, 89, 154, 11, 252, 50, 56, 150, 36, 210, + 197 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "MEQCIEfsraHkCSef4rcU2ysSZxS4imcDKxvRJH6TXEt7cf9QAiBVpICpfS292M+PdYUpbmK1OPxzW2HWcrRWW5od9KgiXgE=", + "A1zjZr/QH950JWL3zF5q4SXsK6yGLTw6EdLXCxs7qprg" + ], + "Sequence": 4294967295 + } + ], + "TxOut": [ + { "Value": 10000, "PkScript": "ABTaquDT3p2P3uMWYeYa6oKLWb54ZA==" }, + { "Value": 0, "PkScript": "ahRn7QvMThJWvCzofSLhkNY6EgEUvw==" }, + { "Value": 74975, "PkScript": "ABTR7GmCiu3FRjdYPgWWQ67j+GLNIw==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testutils/mocks/btc_rpc.go b/zetaclient/testutils/mocks/btc_rpc.go index 56ed211dda..487f4b0632 100644 --- a/zetaclient/testutils/mocks/btc_rpc.go +++ b/zetaclient/testutils/mocks/btc_rpc.go @@ -1,175 +1,548 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + package mocks import ( - "errors" + btcjson "github.com/btcsuite/btcd/btcjson" + btcutil "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" - "github.com/btcsuite/btcd/wire" + chainhash "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" -) + mock "github.com/stretchr/testify/mock" -// EvmClient interface -var _ interfaces.BTCRPCClient = &MockBTCRPCClient{} + rpcclient "github.com/btcsuite/btcd/rpcclient" -// MockBTCRPCClient is a mock implementation of the BTCRPCClient interface -type MockBTCRPCClient struct { - err error - blockCount int64 - blockHash *chainhash.Hash - blockHeader *wire.BlockHeader - blockVerboseTx *btcjson.GetBlockVerboseTxResult - Txs []*btcutil.Tx -} + wire "github.com/btcsuite/btcd/wire" +) -// NewMockBTCRPCClient creates a new mock BTC RPC client -func NewMockBTCRPCClient() *MockBTCRPCClient { - client := &MockBTCRPCClient{} - return client.Reset() +// BTCRPCClient is an autogenerated mock type for the BTCRPCClient type +type BTCRPCClient struct { + mock.Mock } -// Reset clears the mock data -func (c *MockBTCRPCClient) Reset() *MockBTCRPCClient { - if c.err != nil { - return nil +// CreateWallet provides a mock function with given fields: name, opts +func (_m *BTCRPCClient) CreateWallet(name string, opts ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] } + var _ca []interface{} + _ca = append(_ca, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) - c.Txs = []*btcutil.Tx{} - return c -} + if len(ret) == 0 { + panic("no return value specified for CreateWallet") + } -func (c *MockBTCRPCClient) GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) { - return nil, errors.New("not implemented") -} + var r0 *btcjson.CreateWalletResult + var r1 error + if rf, ok := ret.Get(0).(func(string, ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error)); ok { + return rf(name, opts...) + } + if rf, ok := ret.Get(0).(func(string, ...rpcclient.CreateWalletOpt) *btcjson.CreateWalletResult); ok { + r0 = rf(name, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.CreateWalletResult) + } + } -func (c *MockBTCRPCClient) CreateWallet(_ string, _ ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { - return nil, errors.New("not implemented") -} + if rf, ok := ret.Get(1).(func(string, ...rpcclient.CreateWalletOpt) error); ok { + r1 = rf(name, opts...) + } else { + r1 = ret.Error(1) + } -func (c *MockBTCRPCClient) GetNewAddress(_ string) (btcutil.Address, error) { - return nil, errors.New("not implemented") + return r0, r1 } -func (c *MockBTCRPCClient) GenerateToAddress(_ int64, _ btcutil.Address, _ *int64) ([]*chainhash.Hash, error) { - return nil, errors.New("not implemented") -} +// EstimateSmartFee provides a mock function with given fields: confTarget, mode +func (_m *BTCRPCClient) EstimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { + ret := _m.Called(confTarget, mode) -func (c *MockBTCRPCClient) GetBalance(_ string) (btcutil.Amount, error) { - return 0, errors.New("not implemented") -} + if len(ret) == 0 { + panic("no return value specified for EstimateSmartFee") + } -func (c *MockBTCRPCClient) SendRawTransaction(_ *wire.MsgTx, _ bool) (*chainhash.Hash, error) { - return nil, errors.New("not implemented") -} + var r0 *btcjson.EstimateSmartFeeResult + var r1 error + if rf, ok := ret.Get(0).(func(int64, *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error)); ok { + return rf(confTarget, mode) + } + if rf, ok := ret.Get(0).(func(int64, *btcjson.EstimateSmartFeeMode) *btcjson.EstimateSmartFeeResult); ok { + r0 = rf(confTarget, mode) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.EstimateSmartFeeResult) + } + } -func (c *MockBTCRPCClient) ListUnspent() ([]btcjson.ListUnspentResult, error) { - return nil, errors.New("not implemented") -} + if rf, ok := ret.Get(1).(func(int64, *btcjson.EstimateSmartFeeMode) error); ok { + r1 = rf(confTarget, mode) + } else { + r1 = ret.Error(1) + } -func (c *MockBTCRPCClient) ListUnspentMinMaxAddresses( - _ int, - _ int, - _ []btcutil.Address, -) ([]btcjson.ListUnspentResult, error) { - return nil, errors.New("not implemented") + return r0, r1 } -func (c *MockBTCRPCClient) EstimateSmartFee( - _ int64, - _ *btcjson.EstimateSmartFeeMode, -) (*btcjson.EstimateSmartFeeResult, error) { - return nil, errors.New("not implemented") +// GenerateToAddress provides a mock function with given fields: numBlocks, address, maxTries +func (_m *BTCRPCClient) GenerateToAddress(numBlocks int64, address btcutil.Address, maxTries *int64) ([]*chainhash.Hash, error) { + ret := _m.Called(numBlocks, address, maxTries) + + if len(ret) == 0 { + panic("no return value specified for GenerateToAddress") + } + + var r0 []*chainhash.Hash + var r1 error + if rf, ok := ret.Get(0).(func(int64, btcutil.Address, *int64) ([]*chainhash.Hash, error)); ok { + return rf(numBlocks, address, maxTries) + } + if rf, ok := ret.Get(0).(func(int64, btcutil.Address, *int64) []*chainhash.Hash); ok { + r0 = rf(numBlocks, address, maxTries) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*chainhash.Hash) + } + } + + if rf, ok := ret.Get(1).(func(int64, btcutil.Address, *int64) error); ok { + r1 = rf(numBlocks, address, maxTries) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) GetTransaction(_ *chainhash.Hash) (*btcjson.GetTransactionResult, error) { - return nil, errors.New("not implemented") +// GetBalance provides a mock function with given fields: account +func (_m *BTCRPCClient) GetBalance(account string) (btcutil.Amount, error) { + ret := _m.Called(account) + + if len(ret) == 0 { + panic("no return value specified for GetBalance") + } + + var r0 btcutil.Amount + var r1 error + if rf, ok := ret.Get(0).(func(string) (btcutil.Amount, error)); ok { + return rf(account) + } + if rf, ok := ret.Get(0).(func(string) btcutil.Amount); ok { + r0 = rf(account) + } else { + r0 = ret.Get(0).(btcutil.Amount) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(account) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// GetRawTransaction returns a pre-loaded transaction or nil -func (c *MockBTCRPCClient) GetRawTransaction(_ *chainhash.Hash) (*btcutil.Tx, error) { - // pop a transaction from the list - if len(c.Txs) > 0 { - tx := c.Txs[len(c.Txs)-1] - c.Txs = c.Txs[:len(c.Txs)-1] - return tx, nil +// GetBlockCount provides a mock function with given fields: +func (_m *BTCRPCClient) GetBlockCount() (int64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetBlockCount") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func() (int64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) } - return nil, errors.New("no transaction found") + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) GetRawTransactionVerbose(_ *chainhash.Hash) (*btcjson.TxRawResult, error) { - return nil, errors.New("not implemented") +// GetBlockHash provides a mock function with given fields: blockHeight +func (_m *BTCRPCClient) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { + ret := _m.Called(blockHeight) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHash") + } + + var r0 *chainhash.Hash + var r1 error + if rf, ok := ret.Get(0).(func(int64) (*chainhash.Hash, error)); ok { + return rf(blockHeight) + } + if rf, ok := ret.Get(0).(func(int64) *chainhash.Hash); ok { + r0 = rf(blockHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*chainhash.Hash) + } + } + + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(blockHeight) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) GetBlockCount() (int64, error) { - if c.err != nil { - return 0, c.err +// GetBlockHeader provides a mock function with given fields: blockHash +func (_m *BTCRPCClient) GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) { + ret := _m.Called(blockHash) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHeader") } - return c.blockCount, nil + + var r0 *wire.BlockHeader + var r1 error + if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*wire.BlockHeader, error)); ok { + return rf(blockHash) + } + if rf, ok := ret.Get(0).(func(*chainhash.Hash) *wire.BlockHeader); ok { + r0 = rf(blockHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*wire.BlockHeader) + } + } + + if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { + r1 = rf(blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) GetBlockHash(_ int64) (*chainhash.Hash, error) { - if c.err != nil { - return nil, c.err +// GetBlockVerbose provides a mock function with given fields: blockHash +func (_m *BTCRPCClient) GetBlockVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { + ret := _m.Called(blockHash) + + if len(ret) == 0 { + panic("no return value specified for GetBlockVerbose") + } + + var r0 *btcjson.GetBlockVerboseResult + var r1 error + if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.GetBlockVerboseResult, error)); ok { + return rf(blockHash) } - return c.blockHash, nil + if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.GetBlockVerboseResult); ok { + r0 = rf(blockHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetBlockVerboseResult) + } + } + + if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { + r1 = rf(blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) GetBlockVerbose(_ *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { - return nil, errors.New("not implemented") +// GetBlockVerboseTx provides a mock function with given fields: blockHash +func (_m *BTCRPCClient) GetBlockVerboseTx(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { + ret := _m.Called(blockHash) + + if len(ret) == 0 { + panic("no return value specified for GetBlockVerboseTx") + } + + var r0 *btcjson.GetBlockVerboseTxResult + var r1 error + if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error)); ok { + return rf(blockHash) + } + if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.GetBlockVerboseTxResult); ok { + r0 = rf(blockHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetBlockVerboseTxResult) + } + } + + if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { + r1 = rf(blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) GetBlockVerboseTx(_ *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { - if c.err != nil { - return nil, c.err +// GetNetworkInfo provides a mock function with given fields: +func (_m *BTCRPCClient) GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetNetworkInfo") } - return c.blockVerboseTx, nil + + var r0 *btcjson.GetNetworkInfoResult + var r1 error + if rf, ok := ret.Get(0).(func() (*btcjson.GetNetworkInfoResult, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *btcjson.GetNetworkInfoResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetNetworkInfoResult) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) GetBlockHeader(_ *chainhash.Hash) (*wire.BlockHeader, error) { - if c.err != nil { - return nil, c.err +// GetNewAddress provides a mock function with given fields: account +func (_m *BTCRPCClient) GetNewAddress(account string) (btcutil.Address, error) { + ret := _m.Called(account) + + if len(ret) == 0 { + panic("no return value specified for GetNewAddress") + } + + var r0 btcutil.Address + var r1 error + if rf, ok := ret.Get(0).(func(string) (btcutil.Address, error)); ok { + return rf(account) + } + if rf, ok := ret.Get(0).(func(string) btcutil.Address); ok { + r0 = rf(account) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(btcutil.Address) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(account) + } else { + r1 = ret.Error(1) } - return c.blockHeader, nil + + return r0, r1 } -// ---------------------------------------------------------------------------- -// Feed data to the mock BTC RPC client for testing -// ---------------------------------------------------------------------------- +// GetRawTransaction provides a mock function with given fields: txHash +func (_m *BTCRPCClient) GetRawTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) { + ret := _m.Called(txHash) + + if len(ret) == 0 { + panic("no return value specified for GetRawTransaction") + } -func (c *MockBTCRPCClient) WithError(err error) *MockBTCRPCClient { - c.err = err - return c + var r0 *btcutil.Tx + var r1 error + if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcutil.Tx, error)); ok { + return rf(txHash) + } + if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcutil.Tx); ok { + r0 = rf(txHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcutil.Tx) + } + } + + if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { + r1 = rf(txHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) WithBlockCount(blkCnt int64) *MockBTCRPCClient { - c.blockCount = blkCnt - return c +// GetRawTransactionVerbose provides a mock function with given fields: txHash +func (_m *BTCRPCClient) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) { + ret := _m.Called(txHash) + + if len(ret) == 0 { + panic("no return value specified for GetRawTransactionVerbose") + } + + var r0 *btcjson.TxRawResult + var r1 error + if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.TxRawResult, error)); ok { + return rf(txHash) + } + if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.TxRawResult); ok { + r0 = rf(txHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.TxRawResult) + } + } + + if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { + r1 = rf(txHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) WithBlockHash(hash *chainhash.Hash) *MockBTCRPCClient { - c.blockHash = hash - return c +// GetTransaction provides a mock function with given fields: txHash +func (_m *BTCRPCClient) GetTransaction(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, error) { + ret := _m.Called(txHash) + + if len(ret) == 0 { + panic("no return value specified for GetTransaction") + } + + var r0 *btcjson.GetTransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.GetTransactionResult, error)); ok { + return rf(txHash) + } + if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.GetTransactionResult); ok { + r0 = rf(txHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetTransactionResult) + } + } + + if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { + r1 = rf(txHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) WithBlockHeader(header *wire.BlockHeader) *MockBTCRPCClient { - c.blockHeader = header - return c +// ListUnspent provides a mock function with given fields: +func (_m *BTCRPCClient) ListUnspent() ([]btcjson.ListUnspentResult, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ListUnspent") + } + + var r0 []btcjson.ListUnspentResult + var r1 error + if rf, ok := ret.Get(0).(func() ([]btcjson.ListUnspentResult, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []btcjson.ListUnspentResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]btcjson.ListUnspentResult) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) WithBlockVerboseTx(block *btcjson.GetBlockVerboseTxResult) *MockBTCRPCClient { - c.blockVerboseTx = block - return c +// ListUnspentMinMaxAddresses provides a mock function with given fields: minConf, maxConf, addrs +func (_m *BTCRPCClient) ListUnspentMinMaxAddresses(minConf int, maxConf int, addrs []btcutil.Address) ([]btcjson.ListUnspentResult, error) { + ret := _m.Called(minConf, maxConf, addrs) + + if len(ret) == 0 { + panic("no return value specified for ListUnspentMinMaxAddresses") + } + + var r0 []btcjson.ListUnspentResult + var r1 error + if rf, ok := ret.Get(0).(func(int, int, []btcutil.Address) ([]btcjson.ListUnspentResult, error)); ok { + return rf(minConf, maxConf, addrs) + } + if rf, ok := ret.Get(0).(func(int, int, []btcutil.Address) []btcjson.ListUnspentResult); ok { + r0 = rf(minConf, maxConf, addrs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]btcjson.ListUnspentResult) + } + } + + if rf, ok := ret.Get(1).(func(int, int, []btcutil.Address) error); ok { + r1 = rf(minConf, maxConf, addrs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) WithRawTransaction(tx *btcutil.Tx) *MockBTCRPCClient { - c.Txs = append(c.Txs, tx) - return c +// SendRawTransaction provides a mock function with given fields: tx, allowHighFees +func (_m *BTCRPCClient) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { + ret := _m.Called(tx, allowHighFees) + + if len(ret) == 0 { + panic("no return value specified for SendRawTransaction") + } + + var r0 *chainhash.Hash + var r1 error + if rf, ok := ret.Get(0).(func(*wire.MsgTx, bool) (*chainhash.Hash, error)); ok { + return rf(tx, allowHighFees) + } + if rf, ok := ret.Get(0).(func(*wire.MsgTx, bool) *chainhash.Hash); ok { + r0 = rf(tx, allowHighFees) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*chainhash.Hash) + } + } + + if rf, ok := ret.Get(1).(func(*wire.MsgTx, bool) error); ok { + r1 = rf(tx, allowHighFees) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (c *MockBTCRPCClient) WithRawTransactions(txs []*btcutil.Tx) *MockBTCRPCClient { - c.Txs = append(c.Txs, txs...) - return c +// NewBTCRPCClient creates a new instance of BTCRPCClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBTCRPCClient(t interface { + mock.TestingT + Cleanup(func()) +}) *BTCRPCClient { + mock := &BTCRPCClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index 160ec230e5..f79c53e5c8 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/wire" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/gagliardetto/solana-go/rpc" "github.com/onrik/ethrpc" @@ -106,6 +107,16 @@ func LoadEVMBlock(t *testing.T, dir string, chainID int64, blockNumber uint64, t return blockProxy.ToBlock() } +// LoadBTCMsgTx loads archived Bitcoin MsgTx from file +func LoadBTCMsgTx(t *testing.T, dir string, chainID int64, txHash string) *wire.MsgTx { + name := path.Join(dir, TestDataPathBTC, FileNameBTCMsgTx(chainID, txHash)) + + // load archived MsgTx + msgTx := &wire.MsgTx{} + LoadObjectFromJSONFile(t, msgTx, name) + return msgTx +} + // LoadBTCTxRawResult loads archived Bitcoin tx raw result from file func LoadBTCTxRawResult(t *testing.T, dir string, chainID int64, txType string, txHash string) *btcjson.TxRawResult { name := path.Join(dir, TestDataPathBTC, FileNameBTCTxByType(chainID, txType, txHash)) diff --git a/zetaclient/testutils/testrpc/rpc_btc.go b/zetaclient/testutils/testrpc/rpc_btc.go index 059dd25621..57f184a60d 100644 --- a/zetaclient/testutils/testrpc/rpc_btc.go +++ b/zetaclient/testutils/testrpc/rpc_btc.go @@ -3,11 +3,17 @@ package testrpc import ( "fmt" "net/url" + "path" "testing" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/testutils" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" ) // BtcServer represents httptest for Bitcoin RPC. @@ -50,3 +56,25 @@ func formatBitcoinRPCHost(serverURL string) (string, error) { return fmt.Sprintf("%s:%s", u.Hostname(), u.Port()), nil } + +// CreateBTCRPCAndLoadTx is a helper function to load raw txs and feed them to mock rpc client +func CreateBTCRPCAndLoadTx(t *testing.T, dir string, chainID int64, txHashes ...string) interfaces.BTCRPCClient { + // create mock rpc client + rpcClient := mocks.NewBTCRPCClient(t) + + // feed txs to mock rpc client + for _, txHash := range txHashes { + // file name for the archived MsgTx + nameMsgTx := path.Join(dir, testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chainID, txHash)) + + // load archived MsgTx + var msgTx wire.MsgTx + testutils.LoadObjectFromJSONFile(t, &msgTx, nameMsgTx) + + // mock rpc response + tx := btcutil.NewTx(&msgTx) + rpcClient.On("GetRawTransaction", tx.Hash()).Return(tx, nil) + } + + return rpcClient +}