Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: detect memo in btc txn from OP_RETURN and inscription #2533

Merged
merged 50 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
857af38
parse inscription like witness data
bitSmiley Jul 21, 2024
73571d7
more comment
bitSmiley Jul 21, 2024
2751889
remove unused code
bitSmiley Jul 21, 2024
0eeb657
parse inscription
bitSmiley Jul 22, 2024
f69bfa4
Update zetaclient/chains/bitcoin/tx_script.go
bitSmiley Jul 22, 2024
8bc2978
Update zetaclient/chains/bitcoin/observer/inbound.go
bitSmiley Jul 22, 2024
6dbb635
Update zetaclient/chains/bitcoin/tx_script.go
bitSmiley Jul 22, 2024
d8732a3
Update zetaclient/chains/bitcoin/tx_script.go
bitSmiley Jul 23, 2024
c43a053
pull origin
bitSmiley Jul 23, 2024
0b36af7
Merge branch 'feat-btc_inscription' of bitsmiley-github:bitSmiley/nod…
bitSmiley Jul 23, 2024
d2088d2
Update zetaclient/chains/bitcoin/observer/inbound.go
bitSmiley Jul 23, 2024
5173f05
review feedbacks
bitSmiley Jul 23, 2024
93b2c9e
Merge branch 'feat-btc_inscription' of bitsmiley-github:bitSmiley/nod…
bitSmiley Jul 23, 2024
e89fabf
update review feedbacks
bitSmiley Jul 23, 2024
bc349f1
scan op_ret then inscription
bitSmiley Jul 23, 2024
46ceef9
add mainnet txn
bitSmiley Jul 23, 2024
4467e13
Update zetaclient/chains/bitcoin/tx_script.go
bitSmiley Jul 24, 2024
9b2b051
parse inscription like witness data
bitSmiley Jul 21, 2024
5447420
more comment
bitSmiley Jul 21, 2024
d767352
remove unused code
bitSmiley Jul 21, 2024
b83f923
Update zetaclient/chains/bitcoin/tx_script.go
bitSmiley Jul 22, 2024
6fb2dfd
Update zetaclient/chains/bitcoin/observer/inbound.go
bitSmiley Jul 22, 2024
d00d739
Update zetaclient/chains/bitcoin/tx_script.go
bitSmiley Jul 22, 2024
1c44556
Update zetaclient/chains/bitcoin/tx_script.go
bitSmiley Jul 23, 2024
61e8700
pull origin
bitSmiley Jul 23, 2024
59e8adc
Update zetaclient/chains/bitcoin/observer/inbound.go
bitSmiley Jul 23, 2024
25d6c4e
review feedbacks
bitSmiley Jul 23, 2024
8790c55
update review feedbacks
bitSmiley Jul 23, 2024
b482706
update make generate
bitSmiley Jul 25, 2024
1a30f39
fix linter
bitSmiley Jul 26, 2024
06f46e9
remove over flow
bitSmiley Jul 26, 2024
47e851e
Update zetaclient/chains/bitcoin/observer/inbound.go
bitSmiley Jul 30, 2024
898edba
Update zetaclient/chains/bitcoin/tokenizer.go
bitSmiley Jul 30, 2024
0070841
Update zetaclient/chains/bitcoin/tokenizer.go
bitSmiley Jul 30, 2024
57716fa
Update zetaclient/chains/bitcoin/tokenizer.go
bitSmiley Jul 30, 2024
b97c226
Update zetaclient/chains/bitcoin/tokenizer.go
bitSmiley Jul 30, 2024
fb8076c
update review feedback
bitSmiley Jul 30, 2024
2dea0c5
update code commnet
bitSmiley Jul 30, 2024
c424b65
update comment
bitSmiley Jul 30, 2024
abf043d
more comments
bitSmiley Jul 30, 2024
27a3ca2
Update changelog.md
bitSmiley Jul 30, 2024
6cc42ed
Update zetaclient/chains/bitcoin/observer/inbound.go
bitSmiley Jul 31, 2024
1b9187d
Update zetaclient/chains/bitcoin/observer/inbound.go
bitSmiley Jul 31, 2024
a6bae3d
review feedback
bitSmiley Jul 31, 2024
95e450d
Merge branch 'develop' into feat-parse_inscription
bitSmiley Jul 31, 2024
e07437a
Merge branch 'develop' into feat-parse_inscription
bitSmiley Aug 1, 2024
d3f2344
clean up
bitSmiley Aug 2, 2024
35c70b2
Merge branch 'develop' into feat-parse_inscription
bitSmiley Aug 2, 2024
33f14a9
format code
bitSmiley Aug 2, 2024
90d03b0
Merge branch 'feat-parse_inscription' of bitsmiley-github:bitSmiley/n…
bitSmiley Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions zetaclient/chains/bitcoin/observer/witness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package observer

import (
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin"
"github.com/zeta-chain/zetacore/zetaclient/chains/interfaces"
)

// GetBtcEventWithWitness either returns a valid BTCInboundEvent or nil.
// This method supports data with more than 80 bytes by scanning the witness for possible presence of a tapscript.
// It will first prioritize OP_RETURN over tapscript.
func GetBtcEventWithWitness(

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / build-and-test

GetBtcEventWithWitness redeclared in this block

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / build-and-test

GetBtcEventWithWitness redeclared in this block

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / build-and-test

GetBtcEventWithWitness redeclared in this block

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / build-and-test

GetBtcEventWithWitness redeclared in this block

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / lint

GetBtcEventWithWitness redeclared in this block

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / lint

GetBtcEventWithWitness redeclared in this block

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / lint

GetBtcEventWithWitness redeclared in this block

Check failure on line 17 in zetaclient/chains/bitcoin/observer/witness.go

View workflow job for this annotation

GitHub Actions / lint

GetBtcEventWithWitness redeclared in this block
client interfaces.BTCRPCClient,
tx btcjson.TxRawResult,
tssAddress string,
blockNumber uint64,
logger zerolog.Logger,
netParams *chaincfg.Params,
depositorFee float64,
) (*BTCInboundEvent, error) {
if len(tx.Vout) < 1 {
logger.Debug().Msgf("no output %s", tx.Txid)
return nil, nil
}
if len(tx.Vin) == 0 {
logger.Debug().Msgf("no input found for inbound: %s", tx.Txid)
return nil, nil
}

if err := isValidRecipient(tx.Vout[0].ScriptPubKey.Hex, tssAddress, netParams); err != nil {
logger.Debug().Msgf("irrelevant recipient %s for tx %s, err: %s", tx.Vout[0].ScriptPubKey.Hex, tx.Txid, err)
return nil, nil
}

isAmountValid, amount := isValidAmount(tx.Vout[0].Value, depositorFee)
if !isAmountValid {
logger.Info().
Msgf("GetBtcEventWithWitness: btc deposit amount %v in txid %s is less than depositor fee %v", tx.Vout[0].Value, tx.Txid, depositorFee)
return nil, nil
}

// Try to extract the memo from the BTC txn. First try to extract from OP_RETURN
// if not found then try to extract from inscription. Return nil if the above two
// cannot find the memo.
var memo []byte
if candidate := tryExtractOpRet(tx, logger); candidate != nil {
memo = candidate
logger.Debug().Msgf("GetBtcEventWithWitness: found OP_RETURN memo %s in tx %s", hex.EncodeToString(memo), tx.Txid)
} else if candidate = tryExtractInscription(tx, logger); candidate != nil {
memo = candidate
logger.Debug().Msgf("GetBtcEventWithWitness: found inscription memo %s in tx %s", hex.EncodeToString(memo), tx.Txid)
} else {
return nil, errors.Errorf("error getting memo for inbound: %s", tx.Txid)
}

// event found, get sender address
fromAddress, err := GetSenderAddressByVin(client, tx.Vin[0], netParams)
if err != nil {
return nil, errors.Wrapf(err, "error getting sender address for inbound: %s", tx.Txid)
}

return &BTCInboundEvent{
FromAddress: fromAddress,
ToAddress: tssAddress,
Value: amount,
MemoBytes: memo,
BlockNumber: blockNumber,
TxHash: tx.Txid,
}, nil
}

// ParseScriptFromWitness attempts to parse the script from the witness data. Ideally it should be handled by
// bitcoin library, however, it's not found in existing library version. Replace this with actual library implementation
// if libraries are updated.
func ParseScriptFromWitness(witness []string, logger zerolog.Logger) []byte {
length := len(witness)

if length == 0 {
return nil
}

lastElement, err := hex.DecodeString(witness[length-1])
if err != nil {
logger.Debug().Msgf("invalid witness element")
return nil
}

// From BIP341:
// If there are at least two witness elements, and the first byte of
// the last element is 0x50, this last element is called annex a
// and is removed from the witness stack.
if length >= 2 && len(lastElement) > 0 && lastElement[0] == 0x50 {
// account for the extra item removed from the end
witness = witness[:length-1]
}

if len(witness) < 2 {
logger.Debug().Msgf("not script path spending detected, ignore")
return nil
}

// only the script is the focus here, ignore checking control block or whatever else
script, err := hex.DecodeString(witness[len(witness)-2])
if err != nil {
logger.Debug().Msgf("witness script cannot be decoded from hex, ignore")
return nil
}
return script
}

// / Try to extract the memo from the OP_RETURN
func tryExtractOpRet(tx btcjson.TxRawResult, logger zerolog.Logger) []byte {
if len(tx.Vout) < 2 {
logger.Debug().Msgf("txn %s has fewer than 2 outputs, not target OP_RETURN txn", tx.Txid)
return nil
}

memo, found, err := bitcoin.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex, tx.Txid)
if err != nil {
logger.Error().Err(err).Msgf("tryExtractOpRet: error decoding OP_RETURN memo: %s", tx.Vout[1].ScriptPubKey.Hex)
return nil
}

if found {
return memo
}
return nil
}

// / Try to extract the memo from inscription
func tryExtractInscription(tx btcjson.TxRawResult, logger zerolog.Logger) []byte {
for i, input := range tx.Vin {
script := ParseScriptFromWitness(input.Witness, logger)
if script == nil {
continue
}

logger.Debug().Msgf("potential witness script, tx %s, input idx %d", tx.Txid, i)

memo, found, err := bitcoin.DecodeScript(script)
if err != nil || !found {
logger.Debug().Msgf("invalid witness script, tx %s, input idx %d", tx.Txid, i)
continue
}

logger.Debug().Msgf("found memo in inscription, tx %s, input idx %d", tx.Txid, i)
return memo
}

return nil
}

func isValidAmount(
incoming float64,
minimal float64,
) (bool, float64) {
if incoming < minimal {
return false, 0
}
return true, incoming - minimal
}

func isValidRecipient(
script string,
tssAddress string,
netParams *chaincfg.Params,
) error {
receiver, err := bitcoin.DecodeScriptP2WPKH(script, netParams)
if err != nil {
return fmt.Errorf("invalid p2wpkh script detected, %s", err)
}

// skip irrelevant tx to us
if receiver != tssAddress {
return fmt.Errorf("irrelevant recipient, %s", receiver)
}

return nil
}
180 changes: 180 additions & 0 deletions zetaclient/chains/bitcoin/observer/witness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package observer_test

import (
"encoding/hex"
"testing"

"github.com/btcsuite/btcd/chaincfg"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/zetacore/pkg/chains"
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin"
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer"
clientcommon "github.com/zeta-chain/zetacore/zetaclient/common"
"github.com/zeta-chain/zetacore/zetaclient/testutils"
"github.com/zeta-chain/zetacore/zetaclient/testutils/mocks"
)

func TestParseScriptFromWitness(t *testing.T) {
t.Run("decode script ok", func(t *testing.T) {
witness := [3]string{
"3a4b32aef0e6ecc62d185594baf4df186c6d48ec15e72515bf81c1bcc1f04c758f4d54486bc2e7c280e649761d9084dbd2e7cdfb20708a7f8d0f82e5277bba2b",
"20888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3cac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068",
"c0888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3c",
}
expected := "20888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3cac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068"

script := observer.ParseScriptFromWitness(witness[:], log.Logger)
require.NotNil(t, script)
require.Equal(t, hex.EncodeToString(script), expected)
})

t.Run("no witness", func(t *testing.T) {
witness := [0]string{}
script := observer.ParseScriptFromWitness(witness[:], log.Logger)
require.Nil(t, script)
})

t.Run("ignore key spending path", func(t *testing.T) {
witness := [1]string{
"134896c42cd95680b048845847c8054756861ffab7d4abab72f6508d67d1ec0c590287ec2161dd7884983286e1cd56ce65c08a24ee0476ede92678a93b1b180c",
}
script := observer.ParseScriptFromWitness(witness[:], log.Logger)
require.Nil(t, script)
})
}

func TestGetBtcEventFromInscription(t *testing.T) {
// load archived inbound P2WPKH raw result
// https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa
txHash := "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 := bitcoin.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier)

t.Run("decode OP_RETURN ok", func(t *testing.T) {
tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false)

// https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697
preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697"
tx.Vin[0].Txid = preHash
tx.Vin[0].Vout = 2

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,
}

// load previous raw tx so so mock rpc client can return it
rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash)

// get BTC event
event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee)
require.NoError(t, err)
require.Equal(t, eventExpected, event)
})

t.Run("decode inscription ok", func(t *testing.T) {
txHash2 := "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8"
tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false)

preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697"
rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash)

eventExpected := &observer.BTCInboundEvent{
FromAddress: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y",
ToAddress: tssAddress,
Value: tx.Vout[0].Value - depositorFee,
MemoBytes: make([]byte, 600),
BlockNumber: blockNumber,
TxHash: tx.Txid,
}

// get BTC event
event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee)
require.NoError(t, err)
require.Equal(t, event, eventExpected)
})

t.Run("decode inscription ok - mainnet", func(t *testing.T) {
// The input data is from the below mainnet, but output is modified for test case
txHash2 := "7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c"
tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false)

preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697"
tx.Vin[0].Txid = preHash
tx.Vin[0].Sequence = 2
rpcClient := createRPCClientAndLoadTx(t, 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,
}

// get BTC event
event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee)
require.NoError(t, err)
require.Equal(t, event, eventExpected)
})

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()
event, err := observer.GetBtcEventWithWitness(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()
event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee)
require.NoError(t, err)
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()

// get BTC event
event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee)
require.Error(t, err)
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()

// get BTC event
event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee)
require.Error(t, err)
require.Nil(t, event)
})
}
Loading
Loading