Skip to content

Commit

Permalink
Switch from bech32 to hex address format (#1567)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronbuchwald authored Sep 18, 2024
1 parent 909fdf3 commit fd4111b
Show file tree
Hide file tree
Showing 34 changed files with 131 additions and 273 deletions.
2 changes: 1 addition & 1 deletion abi/testdata/transfer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"to": "AQIDBAUGBwgJCgsMDQ4PEBESExQAAAAAAAAAAAAAAAAA",
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
}
2 changes: 1 addition & 1 deletion abi/testdata/transferField.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"transfer": {
"to": "AQIDBAUGBwgJCgsMDQ4PEBESExQAAAAAAAAAAAAAAAAA",
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
}
Expand Down
4 changes: 2 additions & 2 deletions abi/testdata/transfersArray.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"transfers": [
{
"to": "AQIDBAUGBwgJCgsMDQ4PEBESExQAAAAAAAAAAAAAAAAA",
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
},
{
"to": "AQIDBAUGBwgJCgsMDQ4PEBESExQAAAAAAAAAAAAAAAAA",
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
}
Expand Down
4 changes: 1 addition & 3 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ type Controller interface {
DatabasePath() string
Symbol() string
Decimals() uint8
Address(codec.Address) string
ParseAddress(string) (codec.Address, error)
GetParser(string) (chain.Parser, error)
HandleTx(*chain.Transaction, *chain.Result)
LookupBalance(address string, uri string) (uint64, error)
LookupBalance(address codec.Address, uri string) (uint64, error)
}

type Handler struct {
Expand Down
7 changes: 3 additions & 4 deletions cli/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (h *Handler) SetKey() error {
}
utils.Outf("{{cyan}}stored keys:{{/}} %d\n", len(keys))
for i := 0; i < len(keys); i++ {
addrStr := h.c.Address(keys[i].Address)
addrStr := keys[i].Address
balance, err := h.c.LookupBalance(addrStr, uris[0])
if err != nil {
return err
Expand Down Expand Up @@ -66,14 +66,13 @@ func (h *Handler) Balance(checkAllChains bool) error {
}
for _, uri := range uris[:max] {
utils.Outf("{{yellow}}uri:{{/}} %s\n", uri)
addrStr := h.c.Address(addr)
balance, err := h.c.LookupBalance(addrStr, uris[0])
balance, err := h.c.LookupBalance(addr, uris[0])
if err != nil {
return err
}
utils.Outf(
"{{cyan}}address:{{/}} %s {{cyan}}balance:{{/}} %s %s\n",
addrStr,
addr,
utils.FormatBalance(balance, h.c.Decimals()),
h.c.Symbol(),
)
Expand Down
7 changes: 3 additions & 4 deletions cli/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,22 @@ func Bytes(label string) ([]byte, error) {
return hex.DecodeString(hexString)
}

func Address(label string, parseAddress func(string) (codec.Address, error)) (codec.Address, error) {
func Address(label string) (codec.Address, error) {
promptText := promptui.Prompt{
Label: label,
Validate: func(input string) error {
if len(input) == 0 {
return ErrInputEmpty
}
_, err := parseAddress(input)
return err
return nil
},
}
recipient, err := promptText.Run()
if err != nil {
return codec.EmptyAddress, err
}
recipient = strings.TrimSpace(recipient)
return parseAddress(recipient)
return codec.StringToAddress(recipient), nil
}

func String(label string, min int, max int) (string, error) {
Expand Down
7 changes: 3 additions & 4 deletions cli/spam.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type SpamHelper interface {
// interface for the HyperSDK.
CreateClient(uri string) error
GetParser(ctx context.Context) (chain.Parser, error)
LookupBalance(choice int, address string) (uint64, error)
LookupBalance(choice int, address codec.Address) (uint64, error)

// GetTransfer returns a list of actions that sends [amount] to a given [address].
//
Expand Down Expand Up @@ -112,8 +112,7 @@ func (h *Handler) Spam(sh SpamHelper) error {
return err
}
for i := 0; i < len(keys); i++ {
address := h.c.Address(keys[i].Address)
balance, err := sh.LookupBalance(i, address)
balance, err := sh.LookupBalance(i, keys[i].Address)
if err != nil {
return err
}
Expand Down Expand Up @@ -415,7 +414,7 @@ func (h *Handler) Spam(sh SpamHelper) error {
issuerWg.Wait()

// Return funds
utils.Outf("{{yellow}}returning funds to %s{{/}}\n", h.c.Address(key.Address))
utils.Outf("{{yellow}}returning funds to %s{{/}}\n", key.Address)
unitPrices, err = cli.UnitPrices(ctx, false)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cli/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (h *Handler) GetDefaultKey(log bool) (codec.Address, []byte, error) {
return codec.EmptyAddress, nil, err
}
if log {
utils.Outf("{{yellow}}address:{{/}} %s\n", h.c.Address(addr))
utils.Outf("{{yellow}}address:{{/}} %s\n", addr)
}
return addr, priv, nil
}
Expand Down
135 changes: 36 additions & 99 deletions codec/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,16 @@
package codec

import (
"encoding/base64"
"errors"
"encoding/hex"
"fmt"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/formatting/address"
)

const (
AddressLen = 33

// These consts are pulled from BIP-173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
fromBits = 8
toBits = 5
separatorLen = 1
checksumlen = 6
maxBech32Size = 90
)
// TODO: add a checksum to the hex address format (ideally similar to EIP55).
const AddressLen = 33

// Address represents the 33 byte address of a HyperSDK account
type Address [AddressLen]byte

var EmptyAddress = Address{}
Expand All @@ -36,106 +27,52 @@ func CreateAddress(typeID uint8, id ids.ID) Address {
return Address(a)
}

func ToAddress(bytes []byte) (Address, error) {
var result Address
if len(bytes) != AddressLen {
return result, errors.New("bytes are not an address")
}
copy(result[:], bytes)
return result, nil
}

// AddressBech32 returns a Bech32 address from [hrp] and [p].
// This function uses avalanchego's FormatBech32 function.
func AddressBech32(hrp string, p Address) (string, error) {
expanedNum := AddressLen * fromBits
expandedLen := expanedNum / toBits
if expanedNum%toBits != 0 {
expandedLen++
}
addrLen := len(hrp) + separatorLen + expandedLen + checksumlen
if addrLen > maxBech32Size {
return "", fmt.Errorf("%w: max=%d, requested=%d", ErrInvalidSize, maxBech32Size, addrLen)
}
return address.FormatBech32(hrp, p[:])
}

// MustAddressBech32 returns a Bech32 address from [hrp] and [p] or panics.
func MustAddressBech32(hrp string, p Address) string {
addr, err := AddressBech32(hrp, p)
if err != nil {
panic(err)
}
return addr
}

// ParseAddressBech32 parses a Bech32 encoded address string and extracts
// its [AddressBytes]. If there is an error reading the address or
// the hrp value is not valid, ParseAddressBech32 returns an error.
func ParseAddressBech32(hrp, saddr string) (Address, error) {
phrp, addr, err := parseAddressBech32(saddr)
if err != nil {
return EmptyAddress, err
func ToAddress(b []byte) (Address, error) {
var a Address
if len(b) != AddressLen {
return a, fmt.Errorf("failed to convert bytes to address: length of bytes is %d, expected %d", len(b), AddressLen)
}
if phrp != hrp {
return EmptyAddress, ErrIncorrectHRP
}
return addr, nil
copy(a[:], b)
return a, nil
}

// ParseAnyHrpAddressBech32 parses a Bech32 encoded address string and extracts
// its [AddressBytes]. If there is an error reading the address ParseAnyHrpAddressBech32 returns an error.
func ParseAnyHrpAddressBech32(saddr string) (Address, error) {
_, addr, err := parseAddressBech32(saddr)
return addr, err
// StringToAddress returns Address with bytes set to the hex decoding
// of s.
// StringToAddress uses copy, which copies the minimum of
// either AddressLen or the length of the hex decoded string.
func StringToAddress(s string) Address {
b, _ := hex.DecodeString(s)
var a Address
copy(a[:], b)
return a
}

// parseAddressBech32 parses a Bech32 encoded address string and extracts
// its [AddressBytes]. If there is an error reading the address parseAddressBech32 returns an error.
func parseAddressBech32(saddr string) (string, Address, error) {
phrp, p, err := address.ParseBech32(saddr)
if err != nil {
return phrp, EmptyAddress, err
}
// The parsed value may be greater than [minLength] because the
// underlying Bech32 implementation requires bytes to each encode 5 bits
// instead of 8 (and we must pad the input to ensure we fill all bytes):
// https://github.com/btcsuite/btcd/blob/902f797b0c4b3af3f7196d2f5d2343931d1b2bdf/btcutil/bech32/bech32.go#L325-L331
if len(p) < AddressLen {
return phrp, EmptyAddress, ErrInsufficientLength
}
return phrp, Address(p[:AddressLen]), nil
// String implements fmt.Stringer.
func (a Address) String() string {
return hex.EncodeToString(a[:])
}

// MarshalJSON marshals the address as a base64 encoded string
func (a Address) MarshalJSON() ([]byte, error) {
// TODO: use hex https://github.com/ava-labs/hypersdk/issues/1527
return []byte(`"` + base64.StdEncoding.EncodeToString(a[:]) + `"`), nil
// MarshalText returns the hex representation of a.
func (a Address) MarshalText() ([]byte, error) {
result := make([]byte, len(a)*2+2)
copy(result, `0x`)
hex.Encode(result[2:], a[:])
return result, nil
}

// UnmarshalJSON unmarshals the Address from a base64-encoded string.
func (a *Address) UnmarshalJSON(data []byte) error {
// Check if the data starts and ends with quotes
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
return errors.New("invalid JSON format: string must be enclosed in quotes")
// UnmarshalText parses a hex-encoded address.
func (a *Address) UnmarshalText(input []byte) error {
// Check if the input has the '0x' prefix and skip it
if len(input) >= 2 && input[0] == '0' && input[1] == 'x' {
input = input[2:]
}

// Remove quotes from the string
s := string(data[1 : len(data)-1])

// Decode base64 string
decoded, err := base64.StdEncoding.DecodeString(s)
// Decode the hex string
decoded, err := hex.DecodeString(string(input))
if err != nil {
return err
return err // Return the error if the hex string is invalid
}

// Check if the decoded data has the correct length
if len(decoded) != AddressLen {
return ErrInsufficientLength
}

// Copy decoded data to the Address
copy(a[:], decoded)

return nil
}
45 changes: 19 additions & 26 deletions codec/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,38 @@
package codec

import (
"bytes"
"encoding/json"
"testing"

"github.com/ava-labs/avalanchego/ids"
"github.com/btcsuite/btcd/btcutil/bech32"
"github.com/stretchr/testify/require"
)

const hrp = "blah"

func TestIDAddress(t *testing.T) {
func TestAddress(t *testing.T) {
require := require.New(t)
typeID := byte(0)
addrID := ids.GenerateTestID()

id := ids.GenerateTestID()
addrBytes := CreateAddress(0, id)
addr, err := AddressBech32(hrp, addrBytes)
addr := CreateAddress(typeID, addrID)
addrStr, err := addr.MarshalText()
require.NoError(err)

sb, err := ParseAddressBech32(hrp, addr)
require.NoError(err)
require.True(bytes.Equal(addrBytes[:], sb[:]))
var parsedAddr Address
require.NoError(parsedAddr.UnmarshalText(addrStr))
require.Equal(addr, parsedAddr)
}

func TestInvalidAddressHRP(t *testing.T) {
func TestAddressJSON(t *testing.T) {
require := require.New(t)
addr := "blah1859dz2uwazfgahey3j53ef2kqrans0c8cv4l78tda3rjkfw0txns8u2e8k"
typeID := byte(0)
addrID := ids.GenerateTestID()

_, err := ParseAddressBech32("test", addr)
require.ErrorIs(err, ErrIncorrectHRP)
}
addr := CreateAddress(typeID, addrID)

func TestInvalidAddressChecksum(t *testing.T) {
require := require.New(t)
addr := "blah1859dz2uwazfgahey3j53ef2kqrans0c8cv4l78tda3rjkfw0txns8u2e7k"

_, err := ParseAddressBech32(hrp, addr)
require.ErrorIs(err, bech32.ErrInvalidChecksum{
Expected: "8u2e8k",
ExpectedM: "8u2e8kjq64z5",
Actual: "8u2e7k",
})
addrJSONBytes, err := json.Marshal(addr)
require.NoError(err)

var parsedAddr Address
require.NoError(json.Unmarshal(addrJSONBytes, &parsedAddr))
require.Equal(addr, parsedAddr)
}
12 changes: 5 additions & 7 deletions codec/codectest/codec_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@
package codectest

import (
"crypto/rand"
"github.com/ava-labs/avalanchego/ids"

"github.com/ava-labs/hypersdk/codec"
)

// NewRandomAddress returns a random address
// for use during testing
func NewRandomAddress() (codec.Address, error) {
b := make([]byte, codec.AddressLen)
if _, err := rand.Read(b); err != nil {
return codec.EmptyAddress, err
}
return codec.ToAddress(b)
func NewRandomAddress() codec.Address {
typeID := byte(0)
addr := ids.GenerateTestID()
return codec.CreateAddress(typeID, addr)
}
Loading

0 comments on commit fd4111b

Please sign in to comment.