From 857af3839711017f7111053c9023b0946aa9774e Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 21 Jul 2024 23:53:02 +0800 Subject: [PATCH 01/42] parse inscription like witness data --- zetaclient/chains/bitcoin/observer/inbound.go | 36 +++ zetaclient/chains/bitcoin/tx_script.go | 230 ++++++++++++++++++ zetaclient/chains/bitcoin/tx_script_test.go | 63 +++++ 3 files changed, 329 insertions(+) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 47a8177cff..5218d2d9cf 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -472,3 +472,39 @@ func GetBtcEvent( } return nil, nil } + +// 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. +// The format of the tapscript is +func GetBtcEventWithWitness( + rpcClient interfaces.BTCRPCClient, + tx btcjson.TxRawResult, + tssAddress string, + blockNumber uint64, + logger zerolog.Logger, + netParams *chaincfg.Params, + depositorFee float64, +) (*BTCInboundEvent, error) { + // first check for OP_RETURN data + event, err := GetBtcEvent( + rpcClient, + tx, + tssAddress, + blockNumber, + logger, + netParams, + depositorFee, + ) + + if err != nil { // should never happen + return nil, err + } + if event != nil { + return event, nil + } + + // TODO: integrate parsing script + + return nil, nil +} diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index b5f0bed226..b6ff4d2bc7 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -3,6 +3,7 @@ package bitcoin // #nosec G507 ripemd160 required for bitcoin address encoding import ( "bytes" + "encoding/binary" "encoding/hex" "fmt" "strconv" @@ -34,6 +35,12 @@ const ( // LengthScriptP2PKH is the length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] LengthScriptP2PKH = 25 + + // LengthInscriptionEnvelope is the length of the witness tapscript envelope for holding pushed data + LengthInscriptionEnvelope = 34 + + // LengthInscriptionWrapper is the length of the witness tapscript envelope for holding pushed data + LengthInscriptionWrapper = 34 ) // PayToAddrScript creates a new script to pay a transaction output to a the @@ -192,6 +199,23 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { return nil, false, nil } +// DecodeScript decodes memo wrapped in a inscription like script in witness +// returns (memo, found, error) +func DecodeScript(script []byte) ([]byte, bool, error) { + t := makeScriptTokenizer(script) + + if err := checkInscriptionEnvelope(&t); err != nil { + return nil, false, err + } + + memoBytes, err := decodeInscriptionPayload(&t) + if err != nil { + return nil, false, err + } + + return memoBytes, true, nil +} + // EncodeAddress returns a human-readable payment address given a ripemd160 hash // and netID which encodes the bitcoin network and address type. It is used // in both pay-to-pubkey-hash (P2PKH) and pay-to-script-hash (P2SH) address @@ -245,3 +269,209 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai return receiverVout, amount, nil } + +// decodeInscriptionPayload checks the envelope for the script monitoring. The format is +// OP_FALSE +// OP_IF +// +// OP_PUSHDATA_N ... +// +// OP_ENDIF +// +// Note: the data pushed in OP_PUSHDATA_N will always be more than 80 bytes and not greater than 520 bytes. +func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { + if !t.Next() || t.Opcode() != txscript.OP_FALSE { + return nil, fmt.Errorf("OP_FALSE not found") + } + + if !t.Next() || t.Opcode() != txscript.OP_IF { + return nil, fmt.Errorf("OP_IF not found") + } + + memo := make([]byte, 0) + next := byte(txscript.OP_IF) + for { + if !t.Next() { + if t.Err() != nil { + return nil, t.Err() + } + return nil, fmt.Errorf("should contain more data, but script ended") + } + + next = t.Opcode() + + if next == txscript.OP_ENDIF { + break + } + + if next < txscript.OP_DATA_1 || next > txscript.OP_PUSHDATA4 { + return nil, fmt.Errorf("expecting data push, found %d", next) + } + + memo = append(memo, t.Data()...) + } + + return memo, nil +} + +// checkInscriptionEnvelope decodes the envelope for the script monitoring. The format is +// OP_PUSHBYTES_32 <32 bytes> OP_CHECKSIG +func checkInscriptionEnvelope(t *scriptTokenizer) error { + if !t.Next() || t.Opcode() != txscript.OP_DATA_32 { + return fmt.Errorf("cannot obtain public key bytes") + } + + if !t.Next() || t.Opcode() != txscript.OP_CHECKSIG { + return fmt.Errorf("cannot parse OP_CHECKSIG") + } + + return nil +} + +func makeScriptTokenizer(script []byte) scriptTokenizer { + return scriptTokenizer{ + script: script, + offset: 0, + } +} + +// scriptTokenizer is supposed to be replaced by txscript.ScriptTokenizer. However, +// it seems currently the btcsuite version does not have ScriptTokenizer. A simplified +// version of that is implemented here. This is fully compatible with txscript.ScriptTokenizer +// one should consider upgrading txscript and remove this implementation +type scriptTokenizer struct { + script []byte + offset int32 + op byte + data []byte + err error +} + +// Done returns true when either all opcodes have been exhausted or a parse +// failure was encountered and therefore the state has an associated error. +func (t *scriptTokenizer) Done() bool { + return t.err != nil || t.offset >= int32(len(t.script)) +} + +// Data returns the data associated with the most recently successfully parsed +// opcode. +func (t *scriptTokenizer) Data() []byte { + return t.data +} + +// Err returns any errors currently associated with the tokenizer. This will +// only be non-nil in the case a parsing error was encountered. +func (t *scriptTokenizer) Err() error { + return t.err +} + +// Opcode returns the current opcode associated with the tokenizer. +func (t *scriptTokenizer) Opcode() byte { + return t.op +} + +// Next attempts to parse the next opcode and returns whether or not it was +// successful. It will not be successful if invoked when already at the end of +// the script, a parse failure is encountered, or an associated error already +// exists due to a previous parse failure. +// +// In the case of a true return, the parsed opcode and data can be obtained with +// the associated functions and the offset into the script will either point to +// the next opcode or the end of the script if the final opcode was parsed. +// +// In the case of a false return, the parsed opcode and data will be the last +// successfully parsed values (if any) and the offset into the script will +// either point to the failing opcode or the end of the script if the function +// was invoked when already at the end of the script. +// +// Invoking this function when already at the end of the script is not +// considered an error and will simply return false. +func (t *scriptTokenizer) Next() bool { + if t.Done() { + return false + } + + op := t.script[t.offset] + + // Only the following op_code will be encountered: + // OP_PUSHDATA*, OP_DATA_*, OP_CHECKSIG, OP_IF, OP_ENDIF, OP_FALSE + switch { + // No additional data. Note that some of the opcodes, notably OP_1NEGATE, + // OP_0, and OP_[1-16] represent the data themselves. + case op == txscript.OP_FALSE || op == txscript.OP_IF || op == txscript.OP_CHECKSIG || op == txscript.OP_ENDIF: + t.offset++ + t.op = op + t.data = nil + return true + + // Data pushes of specific lengths -- OP_DATA_[1-75]. + case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: + script := t.script[t.offset:] + + // add 2 instead of 1 because script includes the opcode as well + length := int32(op) - txscript.OP_DATA_1 + 2 + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += length + t.op = op + t.data = script[1:length] + + return true + case op > txscript.OP_PUSHDATA4: + t.err = fmt.Errorf("unexpected op code") + return false + // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. + default: + var length int32 + switch op { + case txscript.OP_PUSHDATA1: + length = 1 + case txscript.OP_PUSHDATA2: + length = 2 + default: + length = 4 + } + + script := t.script[t.offset+1:] + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Next -length bytes are little endian length of data. + var dataLen int32 + switch length { + case 1: + dataLen = int32(script[0]) + case 2: + dataLen = int32(binary.LittleEndian.Uint16(script[:2])) + case 4: + dataLen = int32(binary.LittleEndian.Uint32(script[:4])) + default: + t.err = fmt.Errorf("invalid opcode length %d", length) + return false + } + + // Move to the beginning of the data. + script = script[length:] + + // Disallow entries that do not fit script or were sign extended. + if dataLen > int32(len(script)) || dataLen < 0 { + t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ + "has %d remaining", op, dataLen, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += 1 + int32(length) + dataLen + t.op = op + t.data = script[:dataLen] + return true + } +} diff --git a/zetaclient/chains/bitcoin/tx_script_test.go b/zetaclient/chains/bitcoin/tx_script_test.go index eea97fc7b5..f1b17f2119 100644 --- a/zetaclient/chains/bitcoin/tx_script_test.go +++ b/zetaclient/chains/bitcoin/tx_script_test.go @@ -491,3 +491,66 @@ func TestDecodeTSSVoutErrors(t *testing.T) { require.Zero(t, amount) }) } + +func TestDecodeScript(t *testing.T) { + t.Run("should decode longer data ok", func(t *testing.T) { + // 600 bytes of random data generated offline + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ac00634d0802c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f4c50646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe68" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.Nil(t, err) + require.True(t, isFound) + + // the expected memo + expected := "c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe" + require.Equal(t, hex.EncodeToString(memo), expected) + }) + + t.Run("should decode shorter data ok", func(t *testing.T) { + // 81 bytes of random data generated offline + data := "20d6f59371037bf30115d9fd6016f0e3ef552cdfc0367ee20aa9df3158f74aaeb4ac00634c51bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c397160881668" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.Nil(t, err) + require.True(t, isFound) + + // the expected memo + expected := "bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c3971608816" + require.Equal(t, hex.EncodeToString(memo), expected) + }) + + t.Run("decode error due to missing data byte", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "20cabd6ecc0245c40f27ca6299dcd3732287c317f3946734f04e27568fc5334218ac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "should contain more data, but script ended") + require.False(t, isFound) + require.Nil(t, memo) + }) + + t.Run("decode error due to missing data for public key", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "cannot obtain public key bytes") + require.False(t, isFound) + require.Nil(t, memo) + }) + + t.Run("decode error due to missing OP_CHECKSIG", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ab" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "cannot parse OP_CHECKSIG") + require.False(t, isFound) + require.Nil(t, memo) + }) +} From 73571d7ddcdd113f08b67e16526c09a6d5a03152 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 00:05:31 +0800 Subject: [PATCH 02/42] more comment --- zetaclient/chains/bitcoin/tx_script.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index b6ff4d2bc7..6910f5b87d 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -201,6 +201,19 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { // DecodeScript decodes memo wrapped in a inscription like script in witness // returns (memo, found, error) +// +// Note: the format of the script is following that of "inscription" defined in ordinal theory. +// However, to separate from inscription (as this use case is not an NFT), simplifications are made. +// The bitcoin envelope script is as follows: +// OP_DATA_32 <32 byte of public key> OP_CHECKSIG +// OP_FALSE +// OP_IF +// +// OP_PUSH 0x... +// OP_PUSH 0x... +// +// OP_ENDIF +// There are no content-type or any other attributes, it's just raw bytes. func DecodeScript(script []byte) ([]byte, bool, error) { t := makeScriptTokenizer(script) From 2751889ace2d11e3bd8cd1c114294c01c4045101 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 00:20:28 +0800 Subject: [PATCH 03/42] remove unused code --- zetaclient/chains/bitcoin/tx_script.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 6910f5b87d..c2fd6d54d6 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -35,12 +35,6 @@ const ( // LengthScriptP2PKH is the length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] LengthScriptP2PKH = 25 - - // LengthInscriptionEnvelope is the length of the witness tapscript envelope for holding pushed data - LengthInscriptionEnvelope = 34 - - // LengthInscriptionWrapper is the length of the witness tapscript envelope for holding pushed data - LengthInscriptionWrapper = 34 ) // PayToAddrScript creates a new script to pay a transaction output to a the @@ -199,7 +193,7 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { return nil, false, nil } -// DecodeScript decodes memo wrapped in a inscription like script in witness +// DecodeScript decodes memo wrapped in an inscription like script in witness // returns (memo, found, error) // // Note: the format of the script is following that of "inscription" defined in ordinal theory. @@ -287,11 +281,13 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai // OP_FALSE // OP_IF // -// OP_PUSHDATA_N ... +// OP_PUSHDATA_N ... +// ... +// OP_PUSHDATA_N ... // // OP_ENDIF // -// Note: the data pushed in OP_PUSHDATA_N will always be more than 80 bytes and not greater than 520 bytes. +// Note: the total data pushed will always be more than 80 bytes and within the btc transaction size limit. func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { if !t.Next() || t.Opcode() != txscript.OP_FALSE { return nil, fmt.Errorf("OP_FALSE not found") @@ -302,7 +298,7 @@ func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { } memo := make([]byte, 0) - next := byte(txscript.OP_IF) + var next byte for { if !t.Next() { if t.Err() != nil { From 0eeb65788f01f573dd439f4fb818eb17ce0d3367 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 15:32:23 +0800 Subject: [PATCH 04/42] parse inscription --- zetaclient/chains/bitcoin/observer/inbound.go | 166 ++++++++++++++++-- .../chains/bitcoin/observer/inbound_test.go | 110 ++++++++++++ ...9550e344bdc14ac38f71fc050096887e535c8.json | 42 +++++ 3 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 5218d2d9cf..5edebb8ce4 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -476,7 +476,6 @@ func GetBtcEvent( // 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. -// The format of the tapscript is func GetBtcEventWithWitness( rpcClient interfaces.BTCRPCClient, tx btcjson.TxRawResult, @@ -486,25 +485,158 @@ func GetBtcEventWithWitness( netParams *chaincfg.Params, depositorFee float64, ) (*BTCInboundEvent, error) { - // first check for OP_RETURN data - event, err := GetBtcEvent( - rpcClient, - tx, - tssAddress, - blockNumber, - logger, - netParams, - depositorFee, - ) + if len(tx.Vout) < 1 { + logger.Debug().Msgf("GetBtcEventWithWitness: no output") + return nil, nil + } + if len(tx.Vin) == 0 { // should never happen + return nil, fmt.Errorf("GetBtcEventWithWitness: no input found for inbound: %s", tx.Txid) + } - if err != nil { // should never happen - return nil, err + if !isValidRecipient(tx.Vout[0].ScriptPubKey.Hex, tssAddress, netParams, logger) { + return nil, nil } - if event != nil { - return event, 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 } - // TODO: integrate parsing script + var memo []byte + if candidate := tryExtractOpRet(tx, logger); candidate != nil { + logger.Debug().Msgf("GetBtcEventWithWitness: found OP_RETURN memo in tx %s", tx.Txid) + memo = candidate + } else if candidate = tryExtractInscription(tx, logger); candidate != nil { + logger.Debug().Msgf("GetBtcEventWithWitness: found inscription memo in tx %s", tx.Txid) + memo = candidate + } else { + return nil, nil + } - return nil, nil + // event found, get sender address + 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) + } + + return &BTCInboundEvent{ + FromAddress: fromAddress, + ToAddress: tssAddress, + Value: amount, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, + }, nil +} + +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 + } + + vout1 := tx.Vout[1] + memo, found, err := bitcoin.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex, tx.Txid) + if err != nil { + logger.Error().Err(err).Msgf("tryExtractOpRet: error decoding OP_RETURN memo: %s", vout1.ScriptPubKey.Hex) + return nil + } + + if found { + return memo + } + return 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 +} + +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, + logger zerolog.Logger, +) bool { + receiver, err := bitcoin.DecodeScriptP2WPKH(script, netParams) + if err != nil { // should never happen + logger.Debug().Msgf("invalid p2wpkh script detected, %s", err) + return false + } + + // skip irrelevant tx to us + if receiver != tssAddress { + logger.Debug().Msgf("irrelevant recipient, %s", receiver) + return false + } + return true } diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index ec121bd9af..af312a668f 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -529,3 +529,113 @@ func TestGetBtcEventErrors(t *testing.T) { require.Nil(t, event) }) } + +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 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("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) + }) +} diff --git a/zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json new file mode 100644 index 0000000000..a4e964500d --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json @@ -0,0 +1,42 @@ +{ + "hex": "020000000001027bc0bba407bc67178f100e352bf6e047fae4cbf960d783586cb5e430b3b700e70000000000feffffff7bc0bba407bc67178f100e352bf6e047fae4cbf960d783586cb5e430b3b700e70100000000feffffff01b4ba0e0000000000160014173fd310e9db2c7e9550ce0f03f1e6c01d833aa90140134896c42cd95680b048845847c8054756861ffab7d4abab72f6508d67d1ec0c590287ec2161dd7884983286e1cd56ce65c08a24ee0476ede92678a93b1b180c03407b5d614a4610bf9196775791fcc589597ca066dcd10048e004cd4c7341bb4bb90cee4705192f3f7db524e8067a5222c7f09baf29ef6b805b8327ecd1e5ab83ca2220f5b059b9a72298ccbefff59d9b943f7e0fc91d8a3b944a95e7b6390cc99eb5f4ac41c0d9dfdf0fe3c83e9870095d67fff59a8056dad28c6dfb944bb71cf64b90ace9a7776b22a1185fb2dc9524f6b178e2693189bf01655d7f38f043923668dc5af45bffd30a00", + "txid": "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8", + "version": 2, + "locktime": 0, + "vin": [ + { + "vout": 0, + "scriptSig": { + "asm": "", + "hex": "" + }, + "sequence": 4294967294, + "txid": "e700b7b330e4b56c5883d760f9cbe4fa47e0f62b350e108f1767bc07a4bbc07b", + "txinwitness": [ + "134896c42cd95680b048845847c8054756861ffab7d4abab72f6508d67d1ec0c590287ec2161dd7884983286e1cd56ce65c08a24ee0476ede92678a93b1b180c" + ] + }, + { + "scriptSig": {"asm": "", "hex": ""}, + "sequence": 4294967294, + "txid": "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697", + "vout": 2, + "txinwitness": [ + "3a4b32aef0e6ecc62d185594baf4df186c6d48ec15e72515bf81c1bcc1f04c758f4d54486bc2e7c280e649761d9084dbd2e7cdfb20708a7f8d0f82e5277bba2b", + "20888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3cac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068", + "c0888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3c" + ] + } + ], + "vout": [ + { + "value": 0.36557203, + "n": 0, + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + } + } + ] +} From f69bfa4ac258a04b8607a277e4c2cc18f9e246a1 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:59:16 +0800 Subject: [PATCH 05/42] Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/tx_script.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index c2fd6d54d6..873efd5711 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -217,7 +217,7 @@ func DecodeScript(script []byte) ([]byte, bool, error) { memoBytes, err := decodeInscriptionPayload(&t) if err != nil { - return nil, false, err + return nil, false, errors.Wrap(err, "unable to decode the payload") } return memoBytes, true, nil From 8bc2978e581b6a7cefe0ba9889b27a0c2bc77642 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:59:36 +0800 Subject: [PATCH 06/42] Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/observer/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 5218d2d9cf..7435ef729e 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -498,7 +498,7 @@ func GetBtcEventWithWitness( ) if err != nil { // should never happen - return nil, err + return nil, errors.Wrap(err, "unable to get btc event") } if event != nil { return event, nil From 6dbb63598ab51ca3abdd9fa5174ab05049219309 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:59:44 +0800 Subject: [PATCH 07/42] Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/tx_script.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 873efd5711..a6a341ae2f 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -212,7 +212,7 @@ func DecodeScript(script []byte) ([]byte, bool, error) { t := makeScriptTokenizer(script) if err := checkInscriptionEnvelope(&t); err != nil { - return nil, false, err + return nil, false, errors.Wrap(err, "unable to check the envelope") } memoBytes, err := decodeInscriptionPayload(&t) From d8732a38082de0d09614cb6bb88c01da5fd19c3c Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:11:23 +0800 Subject: [PATCH 08/42] Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- zetaclient/chains/bitcoin/tx_script.go | 31 +++++--------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index a6a341ae2f..04e793dc3f 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -277,17 +277,6 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai return receiverVout, amount, nil } -// decodeInscriptionPayload checks the envelope for the script monitoring. The format is -// OP_FALSE -// OP_IF -// -// OP_PUSHDATA_N ... -// ... -// OP_PUSHDATA_N ... -// -// OP_ENDIF -// -// Note: the total data pushed will always be more than 80 bytes and within the btc transaction size limit. func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { if !t.Next() || t.Opcode() != txscript.OP_FALSE { return nil, fmt.Errorf("OP_FALSE not found") @@ -299,28 +288,20 @@ func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { memo := make([]byte, 0) var next byte - for { - if !t.Next() { - if t.Err() != nil { - return nil, t.Err() - } - return nil, fmt.Errorf("should contain more data, but script ended") - } - + for t.Next() { next = t.Opcode() - if next == txscript.OP_ENDIF { - break + return memo, nil } - if next < txscript.OP_DATA_1 || next > txscript.OP_PUSHDATA4 { return nil, fmt.Errorf("expecting data push, found %d", next) } - memo = append(memo, t.Data()...) } - - return memo, nil + if t.Err() != nil { + return nil, t.Err() + } + return nil, fmt.Errorf("should contain more data, but script ended") } // checkInscriptionEnvelope decodes the envelope for the script monitoring. The format is From c43a053e5c08528b10ed95596273108a5e1bac4f Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 23 Jul 2024 09:16:22 +0800 Subject: [PATCH 09/42] pull origin --- zetaclient/chains/bitcoin/tx_script.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index a6a341ae2f..8b2016210e 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -212,12 +212,12 @@ func DecodeScript(script []byte) ([]byte, bool, error) { t := makeScriptTokenizer(script) if err := checkInscriptionEnvelope(&t); err != nil { - return nil, false, errors.Wrap(err, "unable to check the envelope") + return nil, false, errors.Wrap(err, "checkInscriptionEnvelope: unable to check the envelope") } memoBytes, err := decodeInscriptionPayload(&t) if err != nil { - return nil, false, errors.Wrap(err, "unable to decode the payload") + return nil, false, errors.Wrap(err, "decodeInscriptionPayload: unable to decode the payload") } return memoBytes, true, nil From d2088d2764f45549fe38dc783b91684cec8e840a Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:18:21 +0800 Subject: [PATCH 10/42] Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/observer/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 7435ef729e..1b25945c16 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -478,7 +478,7 @@ func GetBtcEvent( // It will first prioritize OP_RETURN over tapscript. // The format of the tapscript is func GetBtcEventWithWitness( - rpcClient interfaces.BTCRPCClient, + client interfaces.BTCRPCClient, tx btcjson.TxRawResult, tssAddress string, blockNumber uint64, From 5173f059ba3b45021b586ce36f82c0d814a8eea4 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 23 Jul 2024 09:31:24 +0800 Subject: [PATCH 11/42] review feedbacks --- zetaclient/chains/bitcoin/observer/inbound.go | 1 - zetaclient/chains/bitcoin/tokenizer.go | 155 ++++++++++++++++++ zetaclient/chains/bitcoin/tx_script.go | 155 +----------------- 3 files changed, 158 insertions(+), 153 deletions(-) create mode 100644 zetaclient/chains/bitcoin/tokenizer.go diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 7435ef729e..10d6bf63ae 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -476,7 +476,6 @@ func GetBtcEvent( // 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. -// The format of the tapscript is func GetBtcEventWithWitness( rpcClient interfaces.BTCRPCClient, tx btcjson.TxRawResult, diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go new file mode 100644 index 0000000000..e83694dabc --- /dev/null +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -0,0 +1,155 @@ +package bitcoin + +import ( + "encoding/binary" + "fmt" + "github.com/btcsuite/btcd/txscript" +) + +func newScriptTokenizer(script []byte) scriptTokenizer { + return scriptTokenizer{ + script: script, + offset: 0, + } +} + +// scriptTokenizer is supposed to be replaced by txscript.ScriptTokenizer. However, +// it seems currently the btcsuite version does not have ScriptTokenizer. A simplified +// version of that is implemented here. This is fully compatible with txscript.ScriptTokenizer +// one should consider upgrading txscript and remove this implementation +type scriptTokenizer struct { + script []byte + offset int32 + op byte + data []byte + err error +} + +// Done returns true when either all opcodes have been exhausted or a parse +// failure was encountered and therefore the state has an associated error. +func (t *scriptTokenizer) Done() bool { + return t.err != nil || t.offset >= int32(len(t.script)) +} + +// Data returns the data associated with the most recently successfully parsed +// opcode. +func (t *scriptTokenizer) Data() []byte { + return t.data +} + +// Err returns any errors currently associated with the tokenizer. This will +// only be non-nil in the case a parsing error was encountered. +func (t *scriptTokenizer) Err() error { + return t.err +} + +// Opcode returns the current opcode associated with the tokenizer. +func (t *scriptTokenizer) Opcode() byte { + return t.op +} + +// Next attempts to parse the next opcode and returns whether or not it was +// successful. It will not be successful if invoked when already at the end of +// the script, a parse failure is encountered, or an associated error already +// exists due to a previous parse failure. +// +// In the case of a true return, the parsed opcode and data can be obtained with +// the associated functions and the offset into the script will either point to +// the next opcode or the end of the script if the final opcode was parsed. +// +// In the case of a false return, the parsed opcode and data will be the last +// successfully parsed values (if any) and the offset into the script will +// either point to the failing opcode or the end of the script if the function +// was invoked when already at the end of the script. +// +// Invoking this function when already at the end of the script is not +// considered an error and will simply return false. +func (t *scriptTokenizer) Next() bool { + if t.Done() { + return false + } + + op := t.script[t.offset] + + // Only the following op_code will be encountered: + // OP_PUSHDATA*, OP_DATA_*, OP_CHECKSIG, OP_IF, OP_ENDIF, OP_FALSE + switch { + // No additional data. Note that some of the opcodes, notably OP_1NEGATE, + // OP_0, and OP_[1-16] represent the data themselves. + case op == txscript.OP_FALSE || op == txscript.OP_IF || op == txscript.OP_CHECKSIG || op == txscript.OP_ENDIF: + t.offset++ + t.op = op + t.data = nil + return true + + // Data pushes of specific lengths -- OP_DATA_[1-75]. + case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: + script := t.script[t.offset:] + + // add 2 instead of 1 because script includes the opcode as well + length := int32(op) - txscript.OP_DATA_1 + 2 + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += length + t.op = op + t.data = script[1:length] + + return true + case op > txscript.OP_PUSHDATA4: + t.err = fmt.Errorf("unexpected op code %d", op) + return false + // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. + default: + var length int32 + switch op { + case txscript.OP_PUSHDATA1: + length = 1 + case txscript.OP_PUSHDATA2: + length = 2 + default: + length = 4 + } + + script := t.script[t.offset+1:] + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Next -length bytes are little endian length of data. + var dataLen int32 + switch length { + case 1: + dataLen = int32(script[0]) + case 2: + dataLen = int32(binary.LittleEndian.Uint16(script[:2])) + case 4: + dataLen = int32(binary.LittleEndian.Uint32(script[:4])) + default: + t.err = fmt.Errorf("invalid opcode length %d", length) + return false + } + + // Move to the beginning of the data. + script = script[length:] + + // Disallow entries that do not fit script or were sign extended. + if dataLen > int32(len(script)) || dataLen < 0 { + t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ + "has %d remaining", op, dataLen, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += 1 + int32(length) + dataLen + t.op = op + t.data = script[:dataLen] + return true + } +} diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 96c87b0134..8c0ebf5ff4 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -3,7 +3,6 @@ package bitcoin // #nosec G507 ripemd160 required for bitcoin address encoding import ( "bytes" - "encoding/binary" "encoding/hex" "fmt" "strconv" @@ -209,7 +208,7 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { // OP_ENDIF // There are no content-type or any other attributes, it's just raw bytes. func DecodeScript(script []byte) ([]byte, bool, error) { - t := makeScriptTokenizer(script) + t := newScriptTokenizer(script) if err := checkInscriptionEnvelope(&t); err != nil { return nil, false, errors.Wrap(err, "checkInscriptionEnvelope: unable to check the envelope") @@ -308,160 +307,12 @@ func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { // OP_PUSHBYTES_32 <32 bytes> OP_CHECKSIG func checkInscriptionEnvelope(t *scriptTokenizer) error { if !t.Next() || t.Opcode() != txscript.OP_DATA_32 { - return fmt.Errorf("cannot obtain public key bytes") + return fmt.Errorf("cannot obtain public key bytes op %d or err %s", t.Opcode(), t.Err()) } if !t.Next() || t.Opcode() != txscript.OP_CHECKSIG { - return fmt.Errorf("cannot parse OP_CHECKSIG") + return fmt.Errorf("cannot parse OP_CHECKSIG, op %d or err %s", t.Opcode(), t.Err()) } return nil } - -func makeScriptTokenizer(script []byte) scriptTokenizer { - return scriptTokenizer{ - script: script, - offset: 0, - } -} - -// scriptTokenizer is supposed to be replaced by txscript.ScriptTokenizer. However, -// it seems currently the btcsuite version does not have ScriptTokenizer. A simplified -// version of that is implemented here. This is fully compatible with txscript.ScriptTokenizer -// one should consider upgrading txscript and remove this implementation -type scriptTokenizer struct { - script []byte - offset int32 - op byte - data []byte - err error -} - -// Done returns true when either all opcodes have been exhausted or a parse -// failure was encountered and therefore the state has an associated error. -func (t *scriptTokenizer) Done() bool { - return t.err != nil || t.offset >= int32(len(t.script)) -} - -// Data returns the data associated with the most recently successfully parsed -// opcode. -func (t *scriptTokenizer) Data() []byte { - return t.data -} - -// Err returns any errors currently associated with the tokenizer. This will -// only be non-nil in the case a parsing error was encountered. -func (t *scriptTokenizer) Err() error { - return t.err -} - -// Opcode returns the current opcode associated with the tokenizer. -func (t *scriptTokenizer) Opcode() byte { - return t.op -} - -// Next attempts to parse the next opcode and returns whether or not it was -// successful. It will not be successful if invoked when already at the end of -// the script, a parse failure is encountered, or an associated error already -// exists due to a previous parse failure. -// -// In the case of a true return, the parsed opcode and data can be obtained with -// the associated functions and the offset into the script will either point to -// the next opcode or the end of the script if the final opcode was parsed. -// -// In the case of a false return, the parsed opcode and data will be the last -// successfully parsed values (if any) and the offset into the script will -// either point to the failing opcode or the end of the script if the function -// was invoked when already at the end of the script. -// -// Invoking this function when already at the end of the script is not -// considered an error and will simply return false. -func (t *scriptTokenizer) Next() bool { - if t.Done() { - return false - } - - op := t.script[t.offset] - - // Only the following op_code will be encountered: - // OP_PUSHDATA*, OP_DATA_*, OP_CHECKSIG, OP_IF, OP_ENDIF, OP_FALSE - switch { - // No additional data. Note that some of the opcodes, notably OP_1NEGATE, - // OP_0, and OP_[1-16] represent the data themselves. - case op == txscript.OP_FALSE || op == txscript.OP_IF || op == txscript.OP_CHECKSIG || op == txscript.OP_ENDIF: - t.offset++ - t.op = op - t.data = nil - return true - - // Data pushes of specific lengths -- OP_DATA_[1-75]. - case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: - script := t.script[t.offset:] - - // add 2 instead of 1 because script includes the opcode as well - length := int32(op) - txscript.OP_DATA_1 + 2 - if int32(len(script)) < length { - t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ - "has %d remaining", op, length, len(script)) - return false - } - - // Move the offset forward and set the opcode and data accordingly. - t.offset += length - t.op = op - t.data = script[1:length] - - return true - case op > txscript.OP_PUSHDATA4: - t.err = fmt.Errorf("unexpected op code") - return false - // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. - default: - var length int32 - switch op { - case txscript.OP_PUSHDATA1: - length = 1 - case txscript.OP_PUSHDATA2: - length = 2 - default: - length = 4 - } - - script := t.script[t.offset+1:] - if int32(len(script)) < length { - t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ - "has %d remaining", op, length, len(script)) - return false - } - - // Next -length bytes are little endian length of data. - var dataLen int32 - switch length { - case 1: - dataLen = int32(script[0]) - case 2: - dataLen = int32(binary.LittleEndian.Uint16(script[:2])) - case 4: - dataLen = int32(binary.LittleEndian.Uint32(script[:4])) - default: - t.err = fmt.Errorf("invalid opcode length %d", length) - return false - } - - // Move to the beginning of the data. - script = script[length:] - - // Disallow entries that do not fit script or were sign extended. - if dataLen > int32(len(script)) || dataLen < 0 { - t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ - "has %d remaining", op, dataLen, len(script)) - return false - } - - // Move the offset forward and set the opcode and data accordingly. - t.offset += 1 + int32(length) + dataLen - t.op = op - t.data = script[:dataLen] - return true - } -} From e89fabf3ac884e8410973854b517157cfe104e0c Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 23 Jul 2024 09:34:57 +0800 Subject: [PATCH 12/42] update review feedbacks --- zetaclient/chains/bitcoin/observer/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 4c43ec76a3..a338911ae6 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -487,7 +487,7 @@ func GetBtcEventWithWitness( ) (*BTCInboundEvent, error) { // first check for OP_RETURN data event, err := GetBtcEvent( - rpcClient, + client, tx, tssAddress, blockNumber, From 46ceef91e2ce4b6c8c41bb5b1ed3345f51812bd4 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 23 Jul 2024 10:59:50 +0800 Subject: [PATCH 13/42] add mainnet txn --- .../chains/bitcoin/observer/inbound_test.go | 26 +++++++++++++++ ...2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json | 32 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index f88f1373a6..413ebe51c9 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -620,6 +620,32 @@ func TestGetBtcEventFromInscription(t *testing.T) { 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) diff --git a/zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json new file mode 100644 index 0000000000..ab20339421 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json @@ -0,0 +1,32 @@ +{ + "txid": "7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "213403e1efb29349a48ea9717096cf20d6e19091e496052ab591f310f0deebd6", + "vout": 0, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "7a8d20a4bb100ffd6399dc4fa1972e405e0e245775be1fcd7df3d5212d62c8d2e4b5534b3ae508a1f974d8995aac759454de9645f78245b8bee3b90ade86ea70", + "20a7172b841ddb8716fd0afa3400d18bfe1105df132e6938cca651b65ee3e64853ac00634c6472f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c68", + "c1a7172b841ddb8716fd0afa3400d18bfe1105df132e6938cca651b65ee3e64853" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + }, + "value": 0.45, + "n": 0 + } + ] +} \ No newline at end of file From 4467e13743f21b0a1737151514fe2c417a6fb163 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:09:49 +0800 Subject: [PATCH 14/42] Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- zetaclient/chains/bitcoin/tx_script.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 8c0ebf5ff4..3edb547d12 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -277,21 +277,18 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai } func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { - if !t.Next() || t.Opcode() != txscript.OP_FALSE { + if (!t.Next() || t.Opcode() != txscript.OP_FALSE) { return nil, fmt.Errorf("OP_FALSE not found") } - if !t.Next() || t.Opcode() != txscript.OP_IF { + if (!t.Next() || t.Opcode() != txscript.OP_IF) { return nil, fmt.Errorf("OP_IF not found") } memo := make([]byte, 0) var next byte - for t.Next() { + for t.Next() && t.Opcode() != txscript.OP_ENDIF { next = t.Opcode() - if next == txscript.OP_ENDIF { - return memo, nil - } if next < txscript.OP_DATA_1 || next > txscript.OP_PUSHDATA4 { return nil, fmt.Errorf("expecting data push, found %d", next) } From 9b2b05155b253eb0217384cc62440576f7612e55 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 21 Jul 2024 23:53:02 +0800 Subject: [PATCH 15/42] parse inscription like witness data --- zetaclient/chains/bitcoin/observer/inbound.go | 36 +++ zetaclient/chains/bitcoin/tx_script.go | 230 ++++++++++++++++++ zetaclient/chains/bitcoin/tx_script_test.go | 63 +++++ 3 files changed, 329 insertions(+) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 15a3bfdc99..517bfd0d23 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -477,3 +477,39 @@ func GetBtcEvent( } return nil, nil } + +// 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. +// The format of the tapscript is +func GetBtcEventWithWitness( + rpcClient interfaces.BTCRPCClient, + tx btcjson.TxRawResult, + tssAddress string, + blockNumber uint64, + logger zerolog.Logger, + netParams *chaincfg.Params, + depositorFee float64, +) (*BTCInboundEvent, error) { + // first check for OP_RETURN data + event, err := GetBtcEvent( + rpcClient, + tx, + tssAddress, + blockNumber, + logger, + netParams, + depositorFee, + ) + + if err != nil { // should never happen + return nil, err + } + if event != nil { + return event, nil + } + + // TODO: integrate parsing script + + return nil, nil +} diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index b5f0bed226..b6ff4d2bc7 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -3,6 +3,7 @@ package bitcoin // #nosec G507 ripemd160 required for bitcoin address encoding import ( "bytes" + "encoding/binary" "encoding/hex" "fmt" "strconv" @@ -34,6 +35,12 @@ const ( // LengthScriptP2PKH is the length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] LengthScriptP2PKH = 25 + + // LengthInscriptionEnvelope is the length of the witness tapscript envelope for holding pushed data + LengthInscriptionEnvelope = 34 + + // LengthInscriptionWrapper is the length of the witness tapscript envelope for holding pushed data + LengthInscriptionWrapper = 34 ) // PayToAddrScript creates a new script to pay a transaction output to a the @@ -192,6 +199,23 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { return nil, false, nil } +// DecodeScript decodes memo wrapped in a inscription like script in witness +// returns (memo, found, error) +func DecodeScript(script []byte) ([]byte, bool, error) { + t := makeScriptTokenizer(script) + + if err := checkInscriptionEnvelope(&t); err != nil { + return nil, false, err + } + + memoBytes, err := decodeInscriptionPayload(&t) + if err != nil { + return nil, false, err + } + + return memoBytes, true, nil +} + // EncodeAddress returns a human-readable payment address given a ripemd160 hash // and netID which encodes the bitcoin network and address type. It is used // in both pay-to-pubkey-hash (P2PKH) and pay-to-script-hash (P2SH) address @@ -245,3 +269,209 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai return receiverVout, amount, nil } + +// decodeInscriptionPayload checks the envelope for the script monitoring. The format is +// OP_FALSE +// OP_IF +// +// OP_PUSHDATA_N ... +// +// OP_ENDIF +// +// Note: the data pushed in OP_PUSHDATA_N will always be more than 80 bytes and not greater than 520 bytes. +func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { + if !t.Next() || t.Opcode() != txscript.OP_FALSE { + return nil, fmt.Errorf("OP_FALSE not found") + } + + if !t.Next() || t.Opcode() != txscript.OP_IF { + return nil, fmt.Errorf("OP_IF not found") + } + + memo := make([]byte, 0) + next := byte(txscript.OP_IF) + for { + if !t.Next() { + if t.Err() != nil { + return nil, t.Err() + } + return nil, fmt.Errorf("should contain more data, but script ended") + } + + next = t.Opcode() + + if next == txscript.OP_ENDIF { + break + } + + if next < txscript.OP_DATA_1 || next > txscript.OP_PUSHDATA4 { + return nil, fmt.Errorf("expecting data push, found %d", next) + } + + memo = append(memo, t.Data()...) + } + + return memo, nil +} + +// checkInscriptionEnvelope decodes the envelope for the script monitoring. The format is +// OP_PUSHBYTES_32 <32 bytes> OP_CHECKSIG +func checkInscriptionEnvelope(t *scriptTokenizer) error { + if !t.Next() || t.Opcode() != txscript.OP_DATA_32 { + return fmt.Errorf("cannot obtain public key bytes") + } + + if !t.Next() || t.Opcode() != txscript.OP_CHECKSIG { + return fmt.Errorf("cannot parse OP_CHECKSIG") + } + + return nil +} + +func makeScriptTokenizer(script []byte) scriptTokenizer { + return scriptTokenizer{ + script: script, + offset: 0, + } +} + +// scriptTokenizer is supposed to be replaced by txscript.ScriptTokenizer. However, +// it seems currently the btcsuite version does not have ScriptTokenizer. A simplified +// version of that is implemented here. This is fully compatible with txscript.ScriptTokenizer +// one should consider upgrading txscript and remove this implementation +type scriptTokenizer struct { + script []byte + offset int32 + op byte + data []byte + err error +} + +// Done returns true when either all opcodes have been exhausted or a parse +// failure was encountered and therefore the state has an associated error. +func (t *scriptTokenizer) Done() bool { + return t.err != nil || t.offset >= int32(len(t.script)) +} + +// Data returns the data associated with the most recently successfully parsed +// opcode. +func (t *scriptTokenizer) Data() []byte { + return t.data +} + +// Err returns any errors currently associated with the tokenizer. This will +// only be non-nil in the case a parsing error was encountered. +func (t *scriptTokenizer) Err() error { + return t.err +} + +// Opcode returns the current opcode associated with the tokenizer. +func (t *scriptTokenizer) Opcode() byte { + return t.op +} + +// Next attempts to parse the next opcode and returns whether or not it was +// successful. It will not be successful if invoked when already at the end of +// the script, a parse failure is encountered, or an associated error already +// exists due to a previous parse failure. +// +// In the case of a true return, the parsed opcode and data can be obtained with +// the associated functions and the offset into the script will either point to +// the next opcode or the end of the script if the final opcode was parsed. +// +// In the case of a false return, the parsed opcode and data will be the last +// successfully parsed values (if any) and the offset into the script will +// either point to the failing opcode or the end of the script if the function +// was invoked when already at the end of the script. +// +// Invoking this function when already at the end of the script is not +// considered an error and will simply return false. +func (t *scriptTokenizer) Next() bool { + if t.Done() { + return false + } + + op := t.script[t.offset] + + // Only the following op_code will be encountered: + // OP_PUSHDATA*, OP_DATA_*, OP_CHECKSIG, OP_IF, OP_ENDIF, OP_FALSE + switch { + // No additional data. Note that some of the opcodes, notably OP_1NEGATE, + // OP_0, and OP_[1-16] represent the data themselves. + case op == txscript.OP_FALSE || op == txscript.OP_IF || op == txscript.OP_CHECKSIG || op == txscript.OP_ENDIF: + t.offset++ + t.op = op + t.data = nil + return true + + // Data pushes of specific lengths -- OP_DATA_[1-75]. + case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: + script := t.script[t.offset:] + + // add 2 instead of 1 because script includes the opcode as well + length := int32(op) - txscript.OP_DATA_1 + 2 + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += length + t.op = op + t.data = script[1:length] + + return true + case op > txscript.OP_PUSHDATA4: + t.err = fmt.Errorf("unexpected op code") + return false + // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. + default: + var length int32 + switch op { + case txscript.OP_PUSHDATA1: + length = 1 + case txscript.OP_PUSHDATA2: + length = 2 + default: + length = 4 + } + + script := t.script[t.offset+1:] + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Next -length bytes are little endian length of data. + var dataLen int32 + switch length { + case 1: + dataLen = int32(script[0]) + case 2: + dataLen = int32(binary.LittleEndian.Uint16(script[:2])) + case 4: + dataLen = int32(binary.LittleEndian.Uint32(script[:4])) + default: + t.err = fmt.Errorf("invalid opcode length %d", length) + return false + } + + // Move to the beginning of the data. + script = script[length:] + + // Disallow entries that do not fit script or were sign extended. + if dataLen > int32(len(script)) || dataLen < 0 { + t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ + "has %d remaining", op, dataLen, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += 1 + int32(length) + dataLen + t.op = op + t.data = script[:dataLen] + return true + } +} diff --git a/zetaclient/chains/bitcoin/tx_script_test.go b/zetaclient/chains/bitcoin/tx_script_test.go index eea97fc7b5..f1b17f2119 100644 --- a/zetaclient/chains/bitcoin/tx_script_test.go +++ b/zetaclient/chains/bitcoin/tx_script_test.go @@ -491,3 +491,66 @@ func TestDecodeTSSVoutErrors(t *testing.T) { require.Zero(t, amount) }) } + +func TestDecodeScript(t *testing.T) { + t.Run("should decode longer data ok", func(t *testing.T) { + // 600 bytes of random data generated offline + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ac00634d0802c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f4c50646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe68" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.Nil(t, err) + require.True(t, isFound) + + // the expected memo + expected := "c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe" + require.Equal(t, hex.EncodeToString(memo), expected) + }) + + t.Run("should decode shorter data ok", func(t *testing.T) { + // 81 bytes of random data generated offline + data := "20d6f59371037bf30115d9fd6016f0e3ef552cdfc0367ee20aa9df3158f74aaeb4ac00634c51bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c397160881668" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.Nil(t, err) + require.True(t, isFound) + + // the expected memo + expected := "bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c3971608816" + require.Equal(t, hex.EncodeToString(memo), expected) + }) + + t.Run("decode error due to missing data byte", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "20cabd6ecc0245c40f27ca6299dcd3732287c317f3946734f04e27568fc5334218ac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "should contain more data, but script ended") + require.False(t, isFound) + require.Nil(t, memo) + }) + + t.Run("decode error due to missing data for public key", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "cannot obtain public key bytes") + require.False(t, isFound) + require.Nil(t, memo) + }) + + t.Run("decode error due to missing OP_CHECKSIG", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ab" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "cannot parse OP_CHECKSIG") + require.False(t, isFound) + require.Nil(t, memo) + }) +} From 54474202188247477150242d6fd510564ea1081f Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 00:05:31 +0800 Subject: [PATCH 16/42] more comment --- zetaclient/chains/bitcoin/tx_script.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index b6ff4d2bc7..6910f5b87d 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -201,6 +201,19 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { // DecodeScript decodes memo wrapped in a inscription like script in witness // returns (memo, found, error) +// +// Note: the format of the script is following that of "inscription" defined in ordinal theory. +// However, to separate from inscription (as this use case is not an NFT), simplifications are made. +// The bitcoin envelope script is as follows: +// OP_DATA_32 <32 byte of public key> OP_CHECKSIG +// OP_FALSE +// OP_IF +// +// OP_PUSH 0x... +// OP_PUSH 0x... +// +// OP_ENDIF +// There are no content-type or any other attributes, it's just raw bytes. func DecodeScript(script []byte) ([]byte, bool, error) { t := makeScriptTokenizer(script) From d767352ac035869f3689f91057b20243e2f96a1d Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 00:20:28 +0800 Subject: [PATCH 17/42] remove unused code --- zetaclient/chains/bitcoin/tx_script.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 6910f5b87d..c2fd6d54d6 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -35,12 +35,6 @@ const ( // LengthScriptP2PKH is the length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] LengthScriptP2PKH = 25 - - // LengthInscriptionEnvelope is the length of the witness tapscript envelope for holding pushed data - LengthInscriptionEnvelope = 34 - - // LengthInscriptionWrapper is the length of the witness tapscript envelope for holding pushed data - LengthInscriptionWrapper = 34 ) // PayToAddrScript creates a new script to pay a transaction output to a the @@ -199,7 +193,7 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { return nil, false, nil } -// DecodeScript decodes memo wrapped in a inscription like script in witness +// DecodeScript decodes memo wrapped in an inscription like script in witness // returns (memo, found, error) // // Note: the format of the script is following that of "inscription" defined in ordinal theory. @@ -287,11 +281,13 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai // OP_FALSE // OP_IF // -// OP_PUSHDATA_N ... +// OP_PUSHDATA_N ... +// ... +// OP_PUSHDATA_N ... // // OP_ENDIF // -// Note: the data pushed in OP_PUSHDATA_N will always be more than 80 bytes and not greater than 520 bytes. +// Note: the total data pushed will always be more than 80 bytes and within the btc transaction size limit. func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { if !t.Next() || t.Opcode() != txscript.OP_FALSE { return nil, fmt.Errorf("OP_FALSE not found") @@ -302,7 +298,7 @@ func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { } memo := make([]byte, 0) - next := byte(txscript.OP_IF) + var next byte for { if !t.Next() { if t.Err() != nil { From b83f923c3168c9e506998cb8a557747dd5423f12 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:59:16 +0800 Subject: [PATCH 18/42] Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/tx_script.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index c2fd6d54d6..873efd5711 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -217,7 +217,7 @@ func DecodeScript(script []byte) ([]byte, bool, error) { memoBytes, err := decodeInscriptionPayload(&t) if err != nil { - return nil, false, err + return nil, false, errors.Wrap(err, "unable to decode the payload") } return memoBytes, true, nil From 6fb2dfddf407ca89e0f50e1064ec14ed90b40ecf Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:59:36 +0800 Subject: [PATCH 19/42] Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/observer/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 517bfd0d23..3bc532c3a1 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -503,7 +503,7 @@ func GetBtcEventWithWitness( ) if err != nil { // should never happen - return nil, err + return nil, errors.Wrap(err, "unable to get btc event") } if event != nil { return event, nil From d00d739779244875e3d36d26029153141167009a Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:59:44 +0800 Subject: [PATCH 20/42] Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/tx_script.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 873efd5711..a6a341ae2f 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -212,7 +212,7 @@ func DecodeScript(script []byte) ([]byte, bool, error) { t := makeScriptTokenizer(script) if err := checkInscriptionEnvelope(&t); err != nil { - return nil, false, err + return nil, false, errors.Wrap(err, "unable to check the envelope") } memoBytes, err := decodeInscriptionPayload(&t) From 1c445568c8f3ddf8d656b9c4ab1ee6ec868219c4 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:11:23 +0800 Subject: [PATCH 21/42] Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- zetaclient/chains/bitcoin/tx_script.go | 31 +++++--------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index a6a341ae2f..04e793dc3f 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -277,17 +277,6 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai return receiverVout, amount, nil } -// decodeInscriptionPayload checks the envelope for the script monitoring. The format is -// OP_FALSE -// OP_IF -// -// OP_PUSHDATA_N ... -// ... -// OP_PUSHDATA_N ... -// -// OP_ENDIF -// -// Note: the total data pushed will always be more than 80 bytes and within the btc transaction size limit. func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { if !t.Next() || t.Opcode() != txscript.OP_FALSE { return nil, fmt.Errorf("OP_FALSE not found") @@ -299,28 +288,20 @@ func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { memo := make([]byte, 0) var next byte - for { - if !t.Next() { - if t.Err() != nil { - return nil, t.Err() - } - return nil, fmt.Errorf("should contain more data, but script ended") - } - + for t.Next() { next = t.Opcode() - if next == txscript.OP_ENDIF { - break + return memo, nil } - if next < txscript.OP_DATA_1 || next > txscript.OP_PUSHDATA4 { return nil, fmt.Errorf("expecting data push, found %d", next) } - memo = append(memo, t.Data()...) } - - return memo, nil + if t.Err() != nil { + return nil, t.Err() + } + return nil, fmt.Errorf("should contain more data, but script ended") } // checkInscriptionEnvelope decodes the envelope for the script monitoring. The format is From 61e8700fad181a2835e54f27250dc66b75b1adff Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 23 Jul 2024 09:16:22 +0800 Subject: [PATCH 22/42] pull origin --- zetaclient/chains/bitcoin/tx_script.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 04e793dc3f..96c87b0134 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -212,12 +212,12 @@ func DecodeScript(script []byte) ([]byte, bool, error) { t := makeScriptTokenizer(script) if err := checkInscriptionEnvelope(&t); err != nil { - return nil, false, errors.Wrap(err, "unable to check the envelope") + return nil, false, errors.Wrap(err, "checkInscriptionEnvelope: unable to check the envelope") } memoBytes, err := decodeInscriptionPayload(&t) if err != nil { - return nil, false, errors.Wrap(err, "unable to decode the payload") + return nil, false, errors.Wrap(err, "decodeInscriptionPayload: unable to decode the payload") } return memoBytes, true, nil From 59e8adc8d2261eb131aeec50495c7b4d15b979c2 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:18:21 +0800 Subject: [PATCH 23/42] Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> --- zetaclient/chains/bitcoin/observer/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 3bc532c3a1..f56292d886 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -483,7 +483,7 @@ func GetBtcEvent( // It will first prioritize OP_RETURN over tapscript. // The format of the tapscript is func GetBtcEventWithWitness( - rpcClient interfaces.BTCRPCClient, + client interfaces.BTCRPCClient, tx btcjson.TxRawResult, tssAddress string, blockNumber uint64, From 25d6c4eca2438b9b3f41487d68848874731d7b77 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 23 Jul 2024 09:31:24 +0800 Subject: [PATCH 24/42] review feedbacks --- zetaclient/chains/bitcoin/observer/inbound.go | 1 - zetaclient/chains/bitcoin/tokenizer.go | 155 ++++++++++++++++++ zetaclient/chains/bitcoin/tx_script.go | 155 +----------------- 3 files changed, 158 insertions(+), 153 deletions(-) create mode 100644 zetaclient/chains/bitcoin/tokenizer.go diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index f56292d886..89a5a5be70 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -481,7 +481,6 @@ func GetBtcEvent( // 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. -// The format of the tapscript is func GetBtcEventWithWitness( client interfaces.BTCRPCClient, tx btcjson.TxRawResult, diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go new file mode 100644 index 0000000000..e83694dabc --- /dev/null +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -0,0 +1,155 @@ +package bitcoin + +import ( + "encoding/binary" + "fmt" + "github.com/btcsuite/btcd/txscript" +) + +func newScriptTokenizer(script []byte) scriptTokenizer { + return scriptTokenizer{ + script: script, + offset: 0, + } +} + +// scriptTokenizer is supposed to be replaced by txscript.ScriptTokenizer. However, +// it seems currently the btcsuite version does not have ScriptTokenizer. A simplified +// version of that is implemented here. This is fully compatible with txscript.ScriptTokenizer +// one should consider upgrading txscript and remove this implementation +type scriptTokenizer struct { + script []byte + offset int32 + op byte + data []byte + err error +} + +// Done returns true when either all opcodes have been exhausted or a parse +// failure was encountered and therefore the state has an associated error. +func (t *scriptTokenizer) Done() bool { + return t.err != nil || t.offset >= int32(len(t.script)) +} + +// Data returns the data associated with the most recently successfully parsed +// opcode. +func (t *scriptTokenizer) Data() []byte { + return t.data +} + +// Err returns any errors currently associated with the tokenizer. This will +// only be non-nil in the case a parsing error was encountered. +func (t *scriptTokenizer) Err() error { + return t.err +} + +// Opcode returns the current opcode associated with the tokenizer. +func (t *scriptTokenizer) Opcode() byte { + return t.op +} + +// Next attempts to parse the next opcode and returns whether or not it was +// successful. It will not be successful if invoked when already at the end of +// the script, a parse failure is encountered, or an associated error already +// exists due to a previous parse failure. +// +// In the case of a true return, the parsed opcode and data can be obtained with +// the associated functions and the offset into the script will either point to +// the next opcode or the end of the script if the final opcode was parsed. +// +// In the case of a false return, the parsed opcode and data will be the last +// successfully parsed values (if any) and the offset into the script will +// either point to the failing opcode or the end of the script if the function +// was invoked when already at the end of the script. +// +// Invoking this function when already at the end of the script is not +// considered an error and will simply return false. +func (t *scriptTokenizer) Next() bool { + if t.Done() { + return false + } + + op := t.script[t.offset] + + // Only the following op_code will be encountered: + // OP_PUSHDATA*, OP_DATA_*, OP_CHECKSIG, OP_IF, OP_ENDIF, OP_FALSE + switch { + // No additional data. Note that some of the opcodes, notably OP_1NEGATE, + // OP_0, and OP_[1-16] represent the data themselves. + case op == txscript.OP_FALSE || op == txscript.OP_IF || op == txscript.OP_CHECKSIG || op == txscript.OP_ENDIF: + t.offset++ + t.op = op + t.data = nil + return true + + // Data pushes of specific lengths -- OP_DATA_[1-75]. + case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: + script := t.script[t.offset:] + + // add 2 instead of 1 because script includes the opcode as well + length := int32(op) - txscript.OP_DATA_1 + 2 + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += length + t.op = op + t.data = script[1:length] + + return true + case op > txscript.OP_PUSHDATA4: + t.err = fmt.Errorf("unexpected op code %d", op) + return false + // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. + default: + var length int32 + switch op { + case txscript.OP_PUSHDATA1: + length = 1 + case txscript.OP_PUSHDATA2: + length = 2 + default: + length = 4 + } + + script := t.script[t.offset+1:] + if int32(len(script)) < length { + t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ + "has %d remaining", op, length, len(script)) + return false + } + + // Next -length bytes are little endian length of data. + var dataLen int32 + switch length { + case 1: + dataLen = int32(script[0]) + case 2: + dataLen = int32(binary.LittleEndian.Uint16(script[:2])) + case 4: + dataLen = int32(binary.LittleEndian.Uint32(script[:4])) + default: + t.err = fmt.Errorf("invalid opcode length %d", length) + return false + } + + // Move to the beginning of the data. + script = script[length:] + + // Disallow entries that do not fit script or were sign extended. + if dataLen > int32(len(script)) || dataLen < 0 { + t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ + "has %d remaining", op, dataLen, len(script)) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += 1 + int32(length) + dataLen + t.op = op + t.data = script[:dataLen] + return true + } +} diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index 96c87b0134..8c0ebf5ff4 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -3,7 +3,6 @@ package bitcoin // #nosec G507 ripemd160 required for bitcoin address encoding import ( "bytes" - "encoding/binary" "encoding/hex" "fmt" "strconv" @@ -209,7 +208,7 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { // OP_ENDIF // There are no content-type or any other attributes, it's just raw bytes. func DecodeScript(script []byte) ([]byte, bool, error) { - t := makeScriptTokenizer(script) + t := newScriptTokenizer(script) if err := checkInscriptionEnvelope(&t); err != nil { return nil, false, errors.Wrap(err, "checkInscriptionEnvelope: unable to check the envelope") @@ -308,160 +307,12 @@ func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { // OP_PUSHBYTES_32 <32 bytes> OP_CHECKSIG func checkInscriptionEnvelope(t *scriptTokenizer) error { if !t.Next() || t.Opcode() != txscript.OP_DATA_32 { - return fmt.Errorf("cannot obtain public key bytes") + return fmt.Errorf("cannot obtain public key bytes op %d or err %s", t.Opcode(), t.Err()) } if !t.Next() || t.Opcode() != txscript.OP_CHECKSIG { - return fmt.Errorf("cannot parse OP_CHECKSIG") + return fmt.Errorf("cannot parse OP_CHECKSIG, op %d or err %s", t.Opcode(), t.Err()) } return nil } - -func makeScriptTokenizer(script []byte) scriptTokenizer { - return scriptTokenizer{ - script: script, - offset: 0, - } -} - -// scriptTokenizer is supposed to be replaced by txscript.ScriptTokenizer. However, -// it seems currently the btcsuite version does not have ScriptTokenizer. A simplified -// version of that is implemented here. This is fully compatible with txscript.ScriptTokenizer -// one should consider upgrading txscript and remove this implementation -type scriptTokenizer struct { - script []byte - offset int32 - op byte - data []byte - err error -} - -// Done returns true when either all opcodes have been exhausted or a parse -// failure was encountered and therefore the state has an associated error. -func (t *scriptTokenizer) Done() bool { - return t.err != nil || t.offset >= int32(len(t.script)) -} - -// Data returns the data associated with the most recently successfully parsed -// opcode. -func (t *scriptTokenizer) Data() []byte { - return t.data -} - -// Err returns any errors currently associated with the tokenizer. This will -// only be non-nil in the case a parsing error was encountered. -func (t *scriptTokenizer) Err() error { - return t.err -} - -// Opcode returns the current opcode associated with the tokenizer. -func (t *scriptTokenizer) Opcode() byte { - return t.op -} - -// Next attempts to parse the next opcode and returns whether or not it was -// successful. It will not be successful if invoked when already at the end of -// the script, a parse failure is encountered, or an associated error already -// exists due to a previous parse failure. -// -// In the case of a true return, the parsed opcode and data can be obtained with -// the associated functions and the offset into the script will either point to -// the next opcode or the end of the script if the final opcode was parsed. -// -// In the case of a false return, the parsed opcode and data will be the last -// successfully parsed values (if any) and the offset into the script will -// either point to the failing opcode or the end of the script if the function -// was invoked when already at the end of the script. -// -// Invoking this function when already at the end of the script is not -// considered an error and will simply return false. -func (t *scriptTokenizer) Next() bool { - if t.Done() { - return false - } - - op := t.script[t.offset] - - // Only the following op_code will be encountered: - // OP_PUSHDATA*, OP_DATA_*, OP_CHECKSIG, OP_IF, OP_ENDIF, OP_FALSE - switch { - // No additional data. Note that some of the opcodes, notably OP_1NEGATE, - // OP_0, and OP_[1-16] represent the data themselves. - case op == txscript.OP_FALSE || op == txscript.OP_IF || op == txscript.OP_CHECKSIG || op == txscript.OP_ENDIF: - t.offset++ - t.op = op - t.data = nil - return true - - // Data pushes of specific lengths -- OP_DATA_[1-75]. - case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: - script := t.script[t.offset:] - - // add 2 instead of 1 because script includes the opcode as well - length := int32(op) - txscript.OP_DATA_1 + 2 - if int32(len(script)) < length { - t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ - "has %d remaining", op, length, len(script)) - return false - } - - // Move the offset forward and set the opcode and data accordingly. - t.offset += length - t.op = op - t.data = script[1:length] - - return true - case op > txscript.OP_PUSHDATA4: - t.err = fmt.Errorf("unexpected op code") - return false - // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. - default: - var length int32 - switch op { - case txscript.OP_PUSHDATA1: - length = 1 - case txscript.OP_PUSHDATA2: - length = 2 - default: - length = 4 - } - - script := t.script[t.offset+1:] - if int32(len(script)) < length { - t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ - "has %d remaining", op, length, len(script)) - return false - } - - // Next -length bytes are little endian length of data. - var dataLen int32 - switch length { - case 1: - dataLen = int32(script[0]) - case 2: - dataLen = int32(binary.LittleEndian.Uint16(script[:2])) - case 4: - dataLen = int32(binary.LittleEndian.Uint32(script[:4])) - default: - t.err = fmt.Errorf("invalid opcode length %d", length) - return false - } - - // Move to the beginning of the data. - script = script[length:] - - // Disallow entries that do not fit script or were sign extended. - if dataLen > int32(len(script)) || dataLen < 0 { - t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ - "has %d remaining", op, dataLen, len(script)) - return false - } - - // Move the offset forward and set the opcode and data accordingly. - t.offset += 1 + int32(length) + dataLen - t.op = op - t.data = script[:dataLen] - return true - } -} From 8790c55e2c5371d60c5f8b6d2803c9511467e350 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 23 Jul 2024 09:34:57 +0800 Subject: [PATCH 25/42] update review feedbacks --- zetaclient/chains/bitcoin/observer/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 89a5a5be70..be61c0ed52 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -492,7 +492,7 @@ func GetBtcEventWithWitness( ) (*BTCInboundEvent, error) { // first check for OP_RETURN data event, err := GetBtcEvent( - rpcClient, + client, tx, tssAddress, blockNumber, From b482706cf65ecd0a7bac4300fe1b732b682e5042 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 25 Jul 2024 17:29:27 +0800 Subject: [PATCH 26/42] update make generate --- zetaclient/chains/bitcoin/tokenizer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index e83694dabc..b5e74fa0a8 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -3,6 +3,7 @@ package bitcoin import ( "encoding/binary" "fmt" + "github.com/btcsuite/btcd/txscript" ) From 1a30f3990feb182cf1bdc84143095672b70ab335 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 26 Jul 2024 15:01:45 +0800 Subject: [PATCH 27/42] fix linter --- zetaclient/chains/bitcoin/tokenizer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index b5e74fa0a8..901240c565 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -148,7 +148,7 @@ func (t *scriptTokenizer) Next() bool { } // Move the offset forward and set the opcode and data accordingly. - t.offset += 1 + int32(length) + dataLen + t.offset += 1 + length + dataLen t.op = op t.data = script[:dataLen] return true From 06f46e957a9803727aa38c18c0c8997860280f8c Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 26 Jul 2024 15:10:11 +0800 Subject: [PATCH 28/42] remove over flow --- zetaclient/chains/bitcoin/tokenizer.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index 901240c565..465773736a 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -20,7 +20,7 @@ func newScriptTokenizer(script []byte) scriptTokenizer { // one should consider upgrading txscript and remove this implementation type scriptTokenizer struct { script []byte - offset int32 + offset int op byte data []byte err error @@ -29,7 +29,7 @@ type scriptTokenizer struct { // Done returns true when either all opcodes have been exhausted or a parse // failure was encountered and therefore the state has an associated error. func (t *scriptTokenizer) Done() bool { - return t.err != nil || t.offset >= int32(len(t.script)) + return t.err != nil || t.offset >= len(t.script) } // Data returns the data associated with the most recently successfully parsed @@ -88,8 +88,8 @@ func (t *scriptTokenizer) Next() bool { script := t.script[t.offset:] // add 2 instead of 1 because script includes the opcode as well - length := int32(op) - txscript.OP_DATA_1 + 2 - if int32(len(script)) < length { + length := int(op) - txscript.OP_DATA_1 + 2 + if len(script) < length { t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ "has %d remaining", op, length, len(script)) return false @@ -106,7 +106,7 @@ func (t *scriptTokenizer) Next() bool { return false // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. default: - var length int32 + var length int switch op { case txscript.OP_PUSHDATA1: length = 1 @@ -117,21 +117,21 @@ func (t *scriptTokenizer) Next() bool { } script := t.script[t.offset+1:] - if int32(len(script)) < length { + if len(script) < length { t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ "has %d remaining", op, length, len(script)) return false } // Next -length bytes are little endian length of data. - var dataLen int32 + var dataLen int switch length { case 1: - dataLen = int32(script[0]) + dataLen = int(script[0]) case 2: - dataLen = int32(binary.LittleEndian.Uint16(script[:2])) + dataLen = int(binary.LittleEndian.Uint16(script[:2])) case 4: - dataLen = int32(binary.LittleEndian.Uint32(script[:4])) + dataLen = int(binary.LittleEndian.Uint32(script[:4])) default: t.err = fmt.Errorf("invalid opcode length %d", length) return false @@ -141,7 +141,7 @@ func (t *scriptTokenizer) Next() bool { script = script[length:] // Disallow entries that do not fit script or were sign extended. - if dataLen > int32(len(script)) || dataLen < 0 { + if dataLen > len(script) || dataLen < 0 { t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ "has %d remaining", op, dataLen, len(script)) return false From 47e851e30f68639a5ce871d2f71bec23252a6321 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:45:10 +0800 Subject: [PATCH 29/42] Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/bitcoin/observer/inbound.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index be61c0ed52..470c56d86f 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -501,9 +501,10 @@ func GetBtcEventWithWitness( depositorFee, ) - if err != nil { // should never happen + if err != nil { return nil, errors.Wrap(err, "unable to get btc event") } + if event != nil { return event, nil } From 898edbae72e373996c376027c3feba66841b6c02 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:49:45 +0800 Subject: [PATCH 30/42] Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/bitcoin/tokenizer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index 465773736a..182445a0aa 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -99,11 +99,12 @@ func (t *scriptTokenizer) Next() bool { t.offset += length t.op = op t.data = script[1:length] - return true + case op > txscript.OP_PUSHDATA4: t.err = fmt.Errorf("unexpected op code %d", op) return false + // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. default: var length int From 007084178871e5d0048b8b826ad21263733dc905 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:51:08 +0800 Subject: [PATCH 31/42] Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/bitcoin/tokenizer.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index 182445a0aa..eb80d18c62 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -90,8 +90,7 @@ func (t *scriptTokenizer) Next() bool { // add 2 instead of 1 because script includes the opcode as well length := int(op) - txscript.OP_DATA_1 + 2 if len(script) < length { - t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ - "has %d remaining", op, length, len(script)) + t.err = fmt.Errorf("opcode %d detected, but script only %d bytes remaining", op, len(script)) return false } From 57716fa75e5b701735278cfef007127965bc22d1 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:51:47 +0800 Subject: [PATCH 32/42] Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/bitcoin/tokenizer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index eb80d18c62..22d202f2bc 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -112,8 +112,10 @@ func (t *scriptTokenizer) Next() bool { length = 1 case txscript.OP_PUSHDATA2: length = 2 + case txscript.OP_PUSHDATA4: + length = 4 default: - length = 4 + return fmt.Errorf("unexpected op code %d", op) } script := t.script[t.offset+1:] From b97c2261f0c1b1dc0f25ef1719f7b4ab0e7da176 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:52:18 +0800 Subject: [PATCH 33/42] Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/bitcoin/tokenizer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index 22d202f2bc..eaabc774a5 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -131,9 +131,9 @@ func (t *scriptTokenizer) Next() bool { case 1: dataLen = int(script[0]) case 2: - dataLen = int(binary.LittleEndian.Uint16(script[:2])) + dataLen = int(binary.LittleEndian.Uint16(script[:length])) case 4: - dataLen = int(binary.LittleEndian.Uint32(script[:4])) + dataLen = int(binary.LittleEndian.Uint32(script[:length])) default: t.err = fmt.Errorf("invalid opcode length %d", length) return false From fb8076cb2a26990fcf26233b36ef07dd38a2e71b Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 30 Jul 2024 11:39:04 +0800 Subject: [PATCH 34/42] update review feedback --- zetaclient/chains/bitcoin/tokenizer.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index eaabc774a5..4c4a6fb429 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -113,9 +113,10 @@ func (t *scriptTokenizer) Next() bool { case txscript.OP_PUSHDATA2: length = 2 case txscript.OP_PUSHDATA4: - length = 4 + length = 4 default: - return fmt.Errorf("unexpected op code %d", op) + t.err = fmt.Errorf("unexpected op code %d", op) + return false } script := t.script[t.offset+1:] From 2dea0c54c79d934f581d699bc7a76e4967af9871 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 30 Jul 2024 11:43:19 +0800 Subject: [PATCH 35/42] update code commnet --- zetaclient/chains/bitcoin/tokenizer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index 4c4a6fb429..3ccafb77d8 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -87,8 +87,10 @@ func (t *scriptTokenizer) Next() bool { case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: script := t.script[t.offset:] + // the length should be: int(op) - txscript.OP_DATA_1 + 2 // add 2 instead of 1 because script includes the opcode as well - length := int(op) - txscript.OP_DATA_1 + 2 + // since txscript.OP_DATA_1 is 1, then length is just int(op) + 1 + length := int(op) + 1 if len(script) < length { t.err = fmt.Errorf("opcode %d detected, but script only %d bytes remaining", op, len(script)) return false From c424b650b21616b7abe7fd04b973c4fb4cc86258 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 30 Jul 2024 21:43:47 +0800 Subject: [PATCH 36/42] update comment --- zetaclient/chains/bitcoin/tokenizer.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index 3ccafb77d8..89a58516db 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -87,9 +87,10 @@ func (t *scriptTokenizer) Next() bool { case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: script := t.script[t.offset:] - // the length should be: int(op) - txscript.OP_DATA_1 + 2 - // add 2 instead of 1 because script includes the opcode as well - // since txscript.OP_DATA_1 is 1, then length is just int(op) + 1 + // The length should be: int(op) - txscript.OP_DATA_1 + 2, i.e. op is txscript.OP_DATA_10, that means + // the data length should be 10, which is txscript.OP_DATA_10 - txscript.OP_DATA_1 + 1. + // Here, 2 instead of 1 because `script` also includes the opcode which means it contains one more byte. + // Since txscript.OP_DATA_1 is 1, then length is just int(op) - 1 + 2 = int(op) + 1 length := int(op) + 1 if len(script) < length { t.err = fmt.Errorf("opcode %d detected, but script only %d bytes remaining", op, len(script)) @@ -123,8 +124,7 @@ func (t *scriptTokenizer) Next() bool { script := t.script[t.offset+1:] if len(script) < length { - t.err = fmt.Errorf("opcode %d requires %d bytes, but script only "+ - "has %d remaining", op, length, len(script)) + t.err = fmt.Errorf("opcode %d requires %d bytes, only %d remaining", op, length, len(script)) return false } @@ -147,8 +147,7 @@ func (t *scriptTokenizer) Next() bool { // Disallow entries that do not fit script or were sign extended. if dataLen > len(script) || dataLen < 0 { - t.err = fmt.Errorf("opcode %d pushes %d bytes, but script only "+ - "has %d remaining", op, dataLen, len(script)) + t.err = fmt.Errorf("opcode %d pushes %d bytes, only %d remaining", op, dataLen, len(script)) return false } From abf043de663b0f0d8cc3de62805b049cd8f9aa11 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 30 Jul 2024 21:46:24 +0800 Subject: [PATCH 37/42] more comments --- zetaclient/chains/bitcoin/tokenizer.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go index 89a58516db..5708bfa250 100644 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -152,6 +152,8 @@ func (t *scriptTokenizer) Next() bool { } // Move the offset forward and set the opcode and data accordingly. + // 1 is the opcode size, which is just 1 byte. int(op) is the opcode value, + // it should not be mixed with the size. t.offset += 1 + length + dataLen t.op = op t.data = script[:dataLen] From 27a3ca2c68b5e789dcaf90d22c754cbb96417a51 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:35:39 +0800 Subject: [PATCH 38/42] Update changelog.md --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d00533e4c7..35e1381426 100644 --- a/changelog.md +++ b/changelog.md @@ -38,6 +38,7 @@ * [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore * [2483](https://github.com/zeta-chain/node/pull/2483) - add priorityFee (gasTipCap) gas to the state * [2567](https://github.com/zeta-chain/node/pull/2567) - add sign latency metric to zetaclient (zetaclient_sign_latency) +* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing ### Refactor @@ -584,4 +585,4 @@ Getting the correct TSS address for Bitcoin now requires proviidng the Bitcoin c ### CI * [1218](https://github.com/zeta-chain/node/pull/1218) - cross-compile release binaries and simplify PR testings -* [1302](https://github.com/zeta-chain/node/pull/1302) - add mainnet builds to goreleaser \ No newline at end of file +* [1302](https://github.com/zeta-chain/node/pull/1302) - add mainnet builds to goreleaser From 6cc42ed14750446232a313fe75fddc343362040f Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:37:49 +0800 Subject: [PATCH 39/42] Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/bitcoin/observer/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index e889887d74..4136dfcdb2 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -489,7 +489,7 @@ func GetBtcEventWithWitness( logger.Debug().Msgf("no output %s", tx.Txid) return nil, nil } - if len(tx.Vin) == 0 { // should never happen + if len(tx.Vin) == 0 { logger.Debug().Msgf("no input found for inbound: %s", tx.Txid) return nil, nil } From 1b9187dd6a2aec543ca1deb2b0956d6fc446f016 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:41:46 +0800 Subject: [PATCH 40/42] Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Francisco de Borja Aranda Castillejo --- zetaclient/chains/bitcoin/observer/inbound.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 4136dfcdb2..a159aaba9a 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -539,10 +539,9 @@ func tryExtractOpRet(tx btcjson.TxRawResult, logger zerolog.Logger) []byte { return nil } - vout1 := tx.Vout[1] - memo, found, err := bitcoin.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex, tx.Txid) + 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", vout1.ScriptPubKey.Hex) + logger.Error().Err(err).Msgf("tryExtractOpRet: error decoding OP_RETURN memo: %s", tx.Vout[1].ScriptPubKey.Hex) return nil } From d3f2344c8069a583ae65bf9b3c86b482e8b91c56 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 2 Aug 2024 10:24:27 +0800 Subject: [PATCH 41/42] clean up --- changelog.md | 3 +- zetaclient/chains/bitcoin/observer/inbound.go | 36 ------------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/changelog.md b/changelog.md index ae097c3163..07dcb21b58 100644 --- a/changelog.md +++ b/changelog.md @@ -43,7 +43,8 @@ * [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore * [2483](https://github.com/zeta-chain/node/pull/2483) - add priorityFee (gasTipCap) gas to the state * [2567](https://github.com/zeta-chain/node/pull/2567) - add sign latency metric to zetaclient (zetaclient_sign_latency) -* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing +* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing +* [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription ### Refactor diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 470c56d86f..15a3bfdc99 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -477,39 +477,3 @@ func GetBtcEvent( } return nil, nil } - -// 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( - client interfaces.BTCRPCClient, - tx btcjson.TxRawResult, - tssAddress string, - blockNumber uint64, - logger zerolog.Logger, - netParams *chaincfg.Params, - depositorFee float64, -) (*BTCInboundEvent, error) { - // first check for OP_RETURN data - event, err := GetBtcEvent( - client, - tx, - tssAddress, - blockNumber, - logger, - netParams, - depositorFee, - ) - - if err != nil { - return nil, errors.Wrap(err, "unable to get btc event") - } - - if event != nil { - return event, nil - } - - // TODO: integrate parsing script - - return nil, nil -} From 33f14a92bc28f3a911368cef7f1c5699a19ff59e Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 2 Aug 2024 10:43:15 +0800 Subject: [PATCH 42/42] format code --- zetaclient/chains/bitcoin/observer/witness.go | 5 +- .../chains/bitcoin/observer/witness_test.go | 74 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/zetaclient/chains/bitcoin/observer/witness.go b/zetaclient/chains/bitcoin/observer/witness.go index 6245ba6999..0af55c62a9 100644 --- a/zetaclient/chains/bitcoin/observer/witness.go +++ b/zetaclient/chains/bitcoin/observer/witness.go @@ -3,10 +3,12 @@ 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" ) @@ -50,7 +52,8 @@ func GetBtcEventWithWitness( 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) + 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) diff --git a/zetaclient/chains/bitcoin/observer/witness_test.go b/zetaclient/chains/bitcoin/observer/witness_test.go index e1095ced72..4e93fb5cf1 100644 --- a/zetaclient/chains/bitcoin/observer/witness_test.go +++ b/zetaclient/chains/bitcoin/observer/witness_test.go @@ -79,7 +79,15 @@ func TestGetBtcEventFromInscription(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -101,7 +109,15 @@ func TestGetBtcEventFromInscription(t *testing.T) { } // get BTC event - event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) require.NoError(t, err) require.Equal(t, event, eventExpected) }) @@ -116,7 +132,9 @@ func TestGetBtcEventFromInscription(t *testing.T) { tx.Vin[0].Sequence = 2 rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) - memo, _ := hex.DecodeString("72f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c") + memo, _ := hex.DecodeString( + "72f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c", + ) eventExpected := &observer.BTCInboundEvent{ FromAddress: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", ToAddress: tssAddress, @@ -127,7 +145,15 @@ func TestGetBtcEventFromInscription(t *testing.T) { } // get BTC event - event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) require.NoError(t, err) require.Equal(t, event, eventExpected) }) @@ -139,7 +165,15 @@ func TestGetBtcEventFromInscription(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) require.NoError(t, err) require.Nil(t, event) }) @@ -151,7 +185,15 @@ func TestGetBtcEventFromInscription(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) require.NoError(t, err) require.Nil(t, event) }) @@ -162,7 +204,15 @@ func TestGetBtcEventFromInscription(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // get BTC event - event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) require.Error(t, err) require.Nil(t, event) }) @@ -173,7 +223,15 @@ func TestGetBtcEventFromInscription(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // get BTC event - event, err := observer.GetBtcEventWithWitness(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) require.Error(t, err) require.Nil(t, event) })