From 21884ab5a918abb3c22a9631bdb329f4ac329b3c Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Wed, 29 Jan 2025 08:59:02 +0100 Subject: [PATCH] feat: enable inscription parsing on Bitcoin mainnet (#3425) * remove non withness function * update tests * changelog --- changelog.md | 1 + zetaclient/chains/bitcoin/observer/inbound.go | 107 +----------- .../chains/bitcoin/observer/inbound_test.go | 159 ++---------------- 3 files changed, 18 insertions(+), 249 deletions(-) diff --git a/changelog.md b/changelog.md index b594725508..47ff90b4c7 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ * [3357](https://github.com/zeta-chain/node/pull/3357) - cosmos-sdk v.50.x upgrade * [3358](https://github.com/zeta-chain/node/pull/3358) - register aborted CCTX for Bitcoin inbound that carries insufficient depositor fee * [3368](https://github.com/zeta-chain/node/pull/3368) - cli command to fetch inbound ballot from inbound hash added to zetatools. +* [3425](https://github.com/zeta-chain/node/pull/3425) - enable inscription parsing on Bitcoin mainnet ### Refactor diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index b94b7ca04f..96fcd85be4 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -285,7 +285,7 @@ func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *types.Ms } // GetBtcEvent returns a valid BTCInboundEvent or nil -// it uses witness data to extract the sender address, except for mainnet +// it uses witness data to extract the sender address func GetBtcEvent( ctx context.Context, rpc RPC, @@ -296,114 +296,9 @@ func GetBtcEvent( netParams *chaincfg.Params, feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { - if netParams.Name == chaincfg.MainNetParams.Name { - return GetBtcEventWithoutWitness(ctx, rpc, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) - } - return GetBtcEventWithWitness(ctx, rpc, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) } -// GetBtcEventWithoutWitness either returns a valid BTCInboundEvent or nil -// Note: the caller should retry the tx on error (e.g., GetSenderAddressByVin failed) -// TODO(revamp): simplify this function -func GetBtcEventWithoutWitness( - ctx context.Context, - rpc RPC, - tx btcjson.TxRawResult, - tssAddress string, - blockNumber uint64, - logger zerolog.Logger, - netParams *chaincfg.Params, - feeCalculator common.DepositorFeeCalculator, -) (*BTCInboundEvent, error) { - var ( - found bool - value float64 - depositorFee float64 - memo []byte - status = types.InboundStatus_SUCCESS - ) - - // prepare logger fields - lf := map[string]any{ - logs.FieldMethod: "GetBtcEventWithoutWitness", - logs.FieldTx: tx.Txid, - } - - if len(tx.Vout) >= 2 { - // 1st vout must have tss address as receiver with p2wpkh scriptPubKey - vout0 := tx.Vout[0] - script := vout0.ScriptPubKey.Hex - if len(script) == 44 && script[:4] == "0014" { - // P2WPKH output: 0x00 + 20 bytes of pubkey hash - receiver, err := common.DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) - if err != nil { // should never happen - return nil, err - } - - // skip irrelevant tx to us - if receiver != tssAddress { - return nil, nil - } - - // calculate depositor fee - depositorFee, err = feeCalculator(ctx, rpc, &tx, netParams) - if err != nil { - return nil, errors.Wrapf(err, "error calculating depositor fee for inbound %s", tx.Txid) - } - - // deduct depositor fee - // to allow developers to track failed deposit caused by insufficient depositor fee, - // the error message will be forwarded to zetacore to register a failed CCTX - value, err = DeductDepositorFee(vout0.Value, depositorFee) - if err != nil { - value = 0 - status = types.InboundStatus_INSUFFICIENT_DEPOSITOR_FEE - logger.Error().Err(err).Fields(lf).Msgf("unable to deduct depositor fee") - } - - // 2nd vout must be a valid OP_RETURN memo - vout1 := tx.Vout[1] - memo, found, err = common.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex) - if err != nil { - lf["memo"] = vout1.ScriptPubKey.Hex - logger.Error().Err(err).Fields(lf).Msgf("unable to decode OP_RETURN memo") - return nil, nil - } - } - } - // event found, get sender address - if found { - if len(tx.Vin) == 0 { // should never happen - return nil, fmt.Errorf("GetBtcEvent: no input found for inbound: %s", tx.Txid) - } - - // get sender address by input (vin) - fromAddress, err := GetSenderAddressByVin(ctx, rpc, 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, - DepositorFee: depositorFee, - MemoBytes: memo, - BlockNumber: blockNumber, - TxHash: tx.Txid, - Status: status, - }, nil - } - return nil, nil -} - // GetSenderAddressByVin get the sender address from the transaction input (vin) func GetSenderAddressByVin( ctx context.Context, diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index 57b69cd48e..4e3951b120 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -274,7 +274,7 @@ func TestGetSenderAddressByVin(t *testing.T) { }) } -func TestGetBtcEventWithoutWitness(t *testing.T) { +func TestGetBtcEvent(t *testing.T) { ctx := context.Background() // load archived inbound P2WPKH raw result @@ -315,7 +315,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -348,7 +348,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -373,7 +373,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -398,7 +398,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -423,7 +423,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -448,7 +448,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -469,7 +469,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event rpcClient := mocks.NewBitcoinClient(t) - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -490,7 +490,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // modify the tx to have Vout[0] a P2SH output tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -505,7 +505,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // append 1 byte to script to make it longer than 22 bytes tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" - event, err = observer.GetBtcEventWithoutWitness( + event, err = observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -526,7 +526,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event rpcClient := mocks.NewBitcoinClient(t) - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -546,7 +546,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event rpcClient := mocks.NewBitcoinClient(t) - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -567,7 +567,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event rpcClient := mocks.NewBitcoinClient(t) - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -588,7 +588,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event rpcClient := mocks.NewBitcoinClient(t) - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -620,7 +620,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { rpcClient.On("GetRawTransaction", mock.Anything, mock.Anything).Return(btcutil.NewTx(msgTx), nil) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -650,48 +650,6 @@ func TestGetBtcEventErrors(t *testing.T) { depositorFee := common.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) feeCalculator := mockDepositFeeCalculator(depositorFee, nil) - t.Run("should return error on invalid Vout[0] script", func(t *testing.T) { - // load tx and modify Vout[0] script to invalid script - tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) - tx.Vout[0].ScriptPubKey.Hex = "0014invalid000000000000000000000000000000000" - - // get BTC event - rpcClient := mocks.NewBitcoinClient(t) - event, err := observer.GetBtcEventWithoutWitness( - ctx, - rpcClient, - *tx, - tssAddress, - blockNumber, - log.Logger, - net, - feeCalculator, - ) - 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.NewBitcoinClient(t) - event, err := observer.GetBtcEventWithoutWitness( - ctx, - rpcClient, - *tx, - tssAddress, - blockNumber, - log.Logger, - net, - feeCalculator, - ) - 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) @@ -701,7 +659,7 @@ func TestGetBtcEventErrors(t *testing.T) { rpcClient.On("GetRawTransaction", mock.Anything, mock.Anything).Return(nil, errors.New("rpc error")) // get BTC event - event, err := observer.GetBtcEventWithoutWitness( + event, err := observer.GetBtcEvent( ctx, rpcClient, *tx, @@ -715,88 +673,3 @@ func TestGetBtcEventErrors(t *testing.T) { require.Nil(t, event) }) } - -func TestGetBtcEvent(t *testing.T) { - ctx := context.Background() - - t.Run("should not decode inbound event with witness with mainnet chain", func(t *testing.T) { - // load archived inbound P2WPKH raw result - // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa - chain := chains.BitcoinMainnet - tssAddress := testutils.TSSAddressBTCMainnet - blockNumber := uint64(835640) - net := &chaincfg.MainNetParams - // 2.992e-05, see avgFeeRate https://mempool.space/api/v1/blocks/835640 - depositorFee := common.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier) - feeCalculator := mockDepositFeeCalculator(depositorFee, nil) - - txHash2 := "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8" - tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false) - rpcClient := mocks.NewBitcoinClient(t) - // get BTC event - event, err := observer.GetBtcEvent( - ctx, - rpcClient, - *tx, - tssAddress, - blockNumber, - log.Logger, - net, - feeCalculator, - ) - require.NoError(t, err) - require.Equal(t, (*observer.BTCInboundEvent)(nil), event) - }) - - t.Run("should support legacy BTC inbound event parsing for mainnet", func(t *testing.T) { - // load archived inbound P2WPKH raw result - // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa - txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" - chain := chains.BitcoinMainnet - - // GetBtcEventWithoutWitness arguments - tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) - tssAddress := testutils.TSSAddressBTCMainnet - blockNumber := uint64(835640) - net := &chaincfg.MainNetParams - - // fee rate of above tx is 28 sat/vB - depositorFee := common.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) - feeCalculator := mockDepositFeeCalculator(depositorFee, nil) - - // 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, // 6192 sataoshis - DepositorFee: depositorFee, - MemoBytes: memo, - BlockNumber: blockNumber, - TxHash: tx.Txid, - } - - // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 - preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" - tx.Vin[0].Txid = preHash - tx.Vin[0].Vout = 2 - eventExpected.FromAddress = "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e" - // load previous raw tx so so mock rpc client can return it - rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, preHash) - - // get BTC event - event, err := observer.GetBtcEvent( - ctx, - rpcClient, - *tx, - tssAddress, - blockNumber, - log.Logger, - net, - feeCalculator, - ) - require.NoError(t, err) - require.Equal(t, eventExpected, event) - }) -}