Skip to content

Commit

Permalink
feat: standard non-evm inbound memo (#2987)
Browse files Browse the repository at this point in the history
* initiated memo package

* add unit tests against memo fields version 0

* added memo unit tests

* use separate file for memo header; add more unit tests

* a few renaming and wrapped err message

* add extra good-to-have check for memo fields validation

* add changelog entry

* fix nosec error

* add two more unit tests for missed lines

* remove redundant dependency github.com/test-go/testify v1.1.4

* enhance codec error message

* a few renaming and move test constant to testutil pkg

* corrected typo and improved unit tests

* fix build

* make receiver address optional

* move bits.go to bits folder; type defines for OpCode and EncodingFormat; add more func descriptions

* move legacy Bitcoin memo decoding to memo package

* move sample functions ABIPack, CompactPack into memo pkg self; remove sample package reference from memo to minimize dependency

* fix unit test compile error
  • Loading branch information
ws4charlie authored Oct 16, 2024
1 parent 7b2bbe7 commit 250b90e
Show file tree
Hide file tree
Showing 24 changed files with 2,954 additions and 70 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* [2919](https://github.com/zeta-chain/node/pull/2919) - add inbound sender to revert context
* [2957](https://github.com/zeta-chain/node/pull/2957) - enable Bitcoin inscription support on testnet
* [2896](https://github.com/zeta-chain/node/pull/2896) - add TON inbound observation
* [2987](https://github.com/zeta-chain/node/pull/2987) - add non-EVM standard inbound memo package

### Refactor

Expand Down
25 changes: 0 additions & 25 deletions pkg/chains/conversion.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package chains

import (
"encoding/hex"
"fmt"

"cosmossdk.io/errors"
"github.com/btcsuite/btcd/chaincfg/chainhash"
ethcommon "github.com/ethereum/go-ethereum/common"
)
Expand All @@ -28,26 +26,3 @@ func StringToHash(chainID int64, hash string, additionalChains []Chain) ([]byte,
}
return nil, fmt.Errorf("cannot convert hash to bytes for chain %d", chainID)
}

// ParseAddressAndData parses the message string into an address and data
// message is hex encoded byte array
// [ contractAddress calldata ]
// [ 20B, variable]
func ParseAddressAndData(message string) (ethcommon.Address, []byte, error) {
if len(message) == 0 {
return ethcommon.Address{}, nil, nil
}

data, err := hex.DecodeString(message)
if err != nil {
return ethcommon.Address{}, nil, errors.Wrap(err, "message should be a hex encoded string")
}

if len(data) < 20 {
return ethcommon.Address{}, data, nil
}

address := ethcommon.BytesToAddress(data[:20])
data = data[20:]
return address, data, nil
}
37 changes: 0 additions & 37 deletions pkg/chains/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package chains

import (
"encoding/hex"
"testing"

"github.com/btcsuite/btcd/chaincfg/chainhash"
Expand Down Expand Up @@ -66,39 +65,3 @@ func TestStringToHash(t *testing.T) {
})
}
}

func TestParseAddressAndData(t *testing.T) {
expectedShortMsgResult, err := hex.DecodeString("1a2b3c4d5e6f708192a3b4c5d6e7f808")
require.NoError(t, err)
tests := []struct {
name string
message string
expectAddr ethcommon.Address
expectData []byte
wantErr bool
}{
{
"valid msg",
"95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5",
ethcommon.HexToAddress("95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5"),
[]byte{},
false,
},
{"empty msg", "", ethcommon.Address{}, nil, false},
{"invalid hex", "invalidHex", ethcommon.Address{}, nil, true},
{"short msg", "1a2b3c4d5e6f708192a3b4c5d6e7f808", ethcommon.Address{}, expectedShortMsgResult, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr, data, err := ParseAddressAndData(tt.message)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectAddr, addr)
require.Equal(t, tt.expectData, data)
}
})
}
}
56 changes: 56 additions & 0 deletions pkg/math/bits/bits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package math

import (
"math/bits"
)

// SetBit sets the bit at the given position (0-7) in the byte to 1
func SetBit(b *byte, position uint8) {
if position > 7 {
return
}

// Example: given b = 0b00000000 and position = 3
// step-1: shift value 1 to left by 3 times: 1 << 3 = 0b00001000
// step-2: make an OR operation with original byte to set the bit: 0b00000000 | 0b00001000 = 0b00001000
*b |= 1 << position
}

// IsBitSet returns true if the bit at the given position (0-7) is set in the byte, false otherwise
func IsBitSet(b byte, position uint8) bool {
if position > 7 {
return false
}
bitMask := byte(1 << position)
return b&bitMask != 0
}

// GetBits extracts the value of bits for a given mask
//
// Example: given b = 0b11011001 and mask = 0b11100000, the function returns 0b110
func GetBits(b byte, mask byte) byte {
extracted := b & mask

// get the number of trailing zero bits
trailingZeros := bits.TrailingZeros8(mask)

// remove trailing zeros
return extracted >> trailingZeros
}

// SetBits sets the value to the bits specified in the mask
//
// Example: given b = 0b00100001 and mask = 0b11100000, and value = 0b110, the function returns 0b11000001
func SetBits(b byte, mask byte, value byte) byte {
// get the number of trailing zero bits in the mask
trailingZeros := bits.TrailingZeros8(mask)

// shift the value left by the number of trailing zeros
valueShifted := value << trailingZeros

// clear the bits in 'b' that correspond to the mask
bCleared := b &^ mask

// Set the bits by ORing the cleared 'b' with the shifted value
return bCleared | valueShifted
}
171 changes: 171 additions & 0 deletions pkg/math/bits/bits_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package math_test

import (
"testing"

"github.com/stretchr/testify/require"
zetabits "github.com/zeta-chain/node/pkg/math/bits"
)

func TestSetBit(t *testing.T) {
tests := []struct {
name string
initial byte
position uint8
expected byte
}{
{
name: "set bit at position 0",
initial: 0b00001000,
position: 0,
expected: 0b00001001,
},
{
name: "set bit at position 7",
initial: 0b00001000,
position: 7,
expected: 0b10001000,
},
{
name: "out of range bit position (no effect)",
initial: 0b00000000,
position: 8, // Out of range
expected: 0b00000000,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := tt.initial
zetabits.SetBit(&b, tt.position)
require.Equal(t, tt.expected, b)
})
}
}

func TestIsBitSet(t *testing.T) {
tests := []struct {
name string
b byte
position uint8
expected bool
}{
{
name: "bit 0 set",
b: 0b00000001,
position: 0,
expected: true,
},
{
name: "bit 7 set",
b: 0b10000000,
position: 7,
expected: true,
},
{
name: "bit 2 not set",
b: 0b00000001,
position: 2,
expected: false,
},
{
name: "bit out of range",
b: 0b00000001,
position: 8, // Position out of range
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := zetabits.IsBitSet(tt.b, tt.position)
require.Equal(t, tt.expected, result)
})
}
}

func TestGetBits(t *testing.T) {
tests := []struct {
name string
b byte
mask byte
expected byte
}{
{
name: "extract upper 3 bits",
b: 0b11011001,
mask: 0b11100000,
expected: 0b110,
},
{
name: "extract middle 3 bits",
b: 0b11011001,
mask: 0b00011100,
expected: 0b110,
},
{
name: "extract lower 3 bits",
b: 0b11011001,
mask: 0b00000111,
expected: 0b001,
},
{
name: "extract no bits",
b: 0b11011001,
mask: 0b00000000,
expected: 0b000,
},
{
name: "extract all bits",
b: 0b11111111,
mask: 0b11111111,
expected: 0b11111111,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := zetabits.GetBits(tt.b, tt.mask)
require.Equal(t, tt.expected, result)
})
}
}

func TestSetBits(t *testing.T) {
tests := []struct {
name string
b byte
mask byte
value byte
expected byte
}{
{
name: "set upper 3 bits",
b: 0b00100001,
mask: 0b11100000,
value: 0b110,
expected: 0b11000001,
},
{
name: "set middle 3 bits",
b: 0b00100001,
mask: 0b00011100,
value: 0b101,
expected: 0b00110101,
},
{
name: "set lower 3 bits",
b: 0b11111100,
mask: 0b00000111,
value: 0b101,
expected: 0b11111101,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := zetabits.SetBits(tt.b, tt.mask, tt.value)
require.Equal(t, tt.expected, result)
})
}
}
52 changes: 52 additions & 0 deletions pkg/memo/arg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package memo

// ArgType is the enum for types supported by the codec
type ArgType string

// Define all the types supported by the codec
const (
ArgTypeBytes ArgType = "bytes"
ArgTypeString ArgType = "string"
ArgTypeAddress ArgType = "address"
)

// CodecArg represents a codec argument
type CodecArg struct {
Name string
Type ArgType
Arg interface{}
}

// NewArg create a new codec argument
func NewArg(name string, argType ArgType, arg interface{}) CodecArg {
return CodecArg{
Name: name,
Type: argType,
Arg: arg,
}
}

// ArgReceiver wraps the receiver address in a CodecArg
func ArgReceiver(arg interface{}) CodecArg {
return NewArg("receiver", ArgTypeAddress, arg)
}

// ArgPayload wraps the payload in a CodecArg
func ArgPayload(arg interface{}) CodecArg {
return NewArg("payload", ArgTypeBytes, arg)
}

// ArgRevertAddress wraps the revert address in a CodecArg
func ArgRevertAddress(arg interface{}) CodecArg {
return NewArg("revertAddress", ArgTypeString, arg)
}

// ArgAbortAddress wraps the abort address in a CodecArg
func ArgAbortAddress(arg interface{}) CodecArg {
return NewArg("abortAddress", ArgTypeAddress, arg)
}

// ArgRevertMessage wraps the revert message in a CodecArg
func ArgRevertMessage(arg interface{}) CodecArg {
return NewArg("revertMessage", ArgTypeBytes, arg)
}
Loading

0 comments on commit 250b90e

Please sign in to comment.