Skip to content

Commit 41a10c9

Browse files
committed
feat: address codec with signed commit
1 parent 8771646 commit 41a10c9

File tree

5 files changed

+301
-5
lines changed

5 files changed

+301
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444

4545
### FEATURES
4646

47+
- [\#665](https://github.com/cosmos/evm/pull/665) Add EvmCodec address codec implementation
4748
- [\#346](https://github.com/cosmos/evm/pull/346) Add eth_createAccessList method and implementation
4849
- [\#502](https://github.com/cosmos/evm/pull/502) Add block time in derived logs.
4950
- [\#633](https://github.com/cosmos/evm/pull/633) go-ethereum metrics are now emitted on a separate server. default address: 127.0.0.1:8100.

utils/address_codec.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package utils
2+
3+
import (
4+
"strings"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
8+
"cosmossdk.io/core/address"
9+
10+
sdk "github.com/cosmos/cosmos-sdk/types"
11+
"github.com/cosmos/cosmos-sdk/types/bech32"
12+
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
13+
)
14+
15+
// EvmCodec defines an address codec for EVM compatible cosmos modules
16+
type EvmCodec struct {
17+
Bech32Prefix string
18+
}
19+
20+
var _ address.Codec = (*EvmCodec)(nil)
21+
22+
// NewEvmCodec returns a new EvmCodec with the given bech32 prefix
23+
func NewEvmCodec(prefix string) address.Codec {
24+
return EvmCodec{prefix}
25+
}
26+
27+
// StringToBytes encodes text to bytes
28+
func (bc EvmCodec) StringToBytes(text string) ([]byte, error) {
29+
if len(strings.TrimSpace(text)) == 0 {
30+
return []byte{}, sdkerrors.ErrInvalidAddress.Wrap("empty address string is not allowed")
31+
}
32+
33+
switch {
34+
case common.IsHexAddress(text):
35+
return common.HexToAddress(text).Bytes(), nil
36+
case IsBech32Address(text):
37+
hrp, bz, err := bech32.DecodeAndConvert(text)
38+
if err != nil {
39+
return nil, err
40+
}
41+
if hrp != bc.Bech32Prefix {
42+
return nil, sdkerrors.ErrLogic.Wrapf("hrp does not match bech32 prefix: expected '%s' got '%s'", bc.Bech32Prefix, hrp)
43+
}
44+
if err := sdk.VerifyAddressFormat(bz); err != nil {
45+
return nil, err
46+
}
47+
return bz, nil
48+
default:
49+
return nil, sdkerrors.ErrUnknownAddress.Wrapf("unknown address format: %s", text)
50+
}
51+
}
52+
53+
// BytesToString decodes bytes to text
54+
func (bc EvmCodec) BytesToString(bz []byte) (string, error) {
55+
return common.BytesToAddress(bz).Hex(), nil
56+
}

utils/address_codec_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package utils_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/cosmos/evm/utils"
10+
11+
sdk "github.com/cosmos/cosmos-sdk/types"
12+
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
13+
)
14+
15+
func TestStringToBytes(t *testing.T) {
16+
config := sdk.GetConfig()
17+
config.SetBech32PrefixForAccount("cosmos", "cosmospub")
18+
addrBz := common.HexToAddress(hex).Bytes()
19+
20+
testCases := []struct {
21+
name string
22+
cdcPrefix string
23+
input string
24+
expBz []byte
25+
expErr error
26+
}{
27+
{
28+
"success: valid bech32 address",
29+
"cosmos",
30+
bech32,
31+
addrBz,
32+
nil,
33+
},
34+
{
35+
"success: valid hex address",
36+
"cosmos",
37+
hex,
38+
addrBz,
39+
nil,
40+
},
41+
{
42+
"failure: invalid bech32 address (wrong prefix)",
43+
"evmos",
44+
bech32,
45+
nil,
46+
sdkerrors.ErrLogic.Wrapf("hrp does not match bech32 prefix: expected '%s' got '%s'", "evmos", "cosmos"),
47+
},
48+
{
49+
"failure: invalid bech32 address (too long)",
50+
"cosmos",
51+
"cosmos10jmp6sgh4cc6zt3e8gw05wavvejgr5pwsjskvvv", // extra char at the end
52+
nil,
53+
sdkerrors.ErrUnknownAddress,
54+
},
55+
{
56+
"failure: invalid bech32 address (invalid format)",
57+
"cosmos",
58+
"cosmos10jmp6sgh4cc6zt3e8gw05wavvejgr5pwsjskv", // missing last char
59+
nil,
60+
sdkerrors.ErrUnknownAddress,
61+
},
62+
{
63+
"failure: invalid hex address (odd length)",
64+
"cosmos",
65+
"0x7cB61D4117AE31a12E393a1Cfa3BaC666481D02", // missing last char
66+
nil,
67+
sdkerrors.ErrUnknownAddress,
68+
},
69+
{
70+
"failure: invalid hex address (even length)",
71+
"cosmos",
72+
"0x7cB61D4117AE31a12E393a1Cfa3BaC666481D0", // missing last 2 char
73+
nil,
74+
sdkerrors.ErrUnknownAddress,
75+
},
76+
{
77+
"failure: invalid hex address (too long)",
78+
"cosmos",
79+
"0x7cB61D4117AE31a12E393a1Cfa3BaC666481D02E00", // extra 2 char at the end
80+
nil,
81+
sdkerrors.ErrUnknownAddress,
82+
},
83+
{
84+
"failure: empty string",
85+
"cosmos",
86+
"",
87+
nil,
88+
sdkerrors.ErrInvalidAddress.Wrap("empty address string is not allowed"),
89+
},
90+
}
91+
92+
for _, tc := range testCases {
93+
t.Run(tc.name, func(t *testing.T) {
94+
cdc := utils.NewEvmCodec(tc.cdcPrefix)
95+
bz, err := cdc.StringToBytes(tc.input)
96+
if tc.expErr == nil {
97+
require.NoError(t, err)
98+
require.Equal(t, tc.expBz, bz)
99+
} else {
100+
require.ErrorContains(t, err, tc.expErr.Error())
101+
}
102+
})
103+
}
104+
}
105+
106+
func TestBytesToString(t *testing.T) {
107+
// Keep the same fixtures as your StringToBytes test
108+
config := sdk.GetConfig()
109+
config.SetBech32PrefixForAccount("cosmos", "cosmospub")
110+
111+
addrBz := common.HexToAddress(hex).Bytes() // 20 bytes
112+
zeroAddr := common.Address{}.Hex() // "0x000..."
113+
114+
// Helper codec (used only where we want to derive bytes from the bech32 string)
115+
cdc := utils.NewEvmCodec("cosmos")
116+
117+
type tc struct {
118+
name string
119+
input func() []byte
120+
expHex string
121+
}
122+
123+
testCases := []tc{
124+
{
125+
name: "success: from 20-byte input (hex-derived)",
126+
input: func() []byte {
127+
return addrBz
128+
},
129+
expHex: common.HexToAddress(hex).Hex(), // checksummed
130+
},
131+
{
132+
name: "success: from bech32-derived bytes",
133+
input: func() []byte {
134+
bz, err := cdc.StringToBytes(bech32)
135+
require.NoError(t, err)
136+
require.Len(t, bz, 20)
137+
return bz
138+
},
139+
expHex: common.HexToAddress(hex).Hex(), // same address as above
140+
},
141+
{
142+
name: "success: empty slice -> zero address",
143+
input: func() []byte {
144+
return []byte{}
145+
},
146+
expHex: zeroAddr,
147+
},
148+
{
149+
name: "success: shorter than 20 bytes -> left-padded to zeroes",
150+
input: func() []byte {
151+
// Drop first byte -> 19 bytes; common.BytesToAddress pads on the left
152+
return addrBz[1:]
153+
},
154+
expHex: common.BytesToAddress(addrBz[1:]).Hex(),
155+
},
156+
{
157+
name: "success: longer than 20 bytes -> rightmost 20 used",
158+
input: func() []byte {
159+
// Prepend one byte; common.BytesToAddress will use last 20 bytes (which are addrBz)
160+
return append([]byte{0xAA}, addrBz...)
161+
},
162+
expHex: common.BytesToAddress(append([]byte{0xAA}, addrBz...)).Hex(),
163+
},
164+
{
165+
name: "success: much longer (32 bytes) -> rightmost 20 used",
166+
input: func() []byte {
167+
prefix := make([]byte, 12) // 12 + 20 = 32
168+
return append(prefix, addrBz...)
169+
},
170+
expHex: common.BytesToAddress(append(make([]byte, 12), addrBz...)).Hex(),
171+
},
172+
}
173+
174+
for _, tc := range testCases {
175+
t.Run(tc.name, func(t *testing.T) {
176+
codec := utils.NewEvmCodec("cosmos")
177+
178+
got, err := codec.BytesToString(tc.input())
179+
require.NoError(t, err)
180+
require.Equal(t, tc.expHex, got)
181+
})
182+
}
183+
}

utils/utils.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
2626
"github.com/cosmos/cosmos-sdk/crypto/types/multisig"
2727
sdk "github.com/cosmos/cosmos-sdk/types"
28+
"github.com/cosmos/cosmos-sdk/types/bech32"
2829
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
2930
)
3031

@@ -127,6 +128,12 @@ func IsSupportedKey(pubkey cryptotypes.PubKey) bool {
127128
}
128129
}
129130

131+
// IsBech32Address checks if the address is a valid bech32 address.
132+
func IsBech32Address(address string) bool {
133+
_, _, err := bech32.DecodeAndConvert(address)
134+
return err == nil
135+
}
136+
130137
// GetAccAddressFromBech32 returns the sdk.Account address of given address,
131138
// while also changing bech32 human readable prefix (HRP) to the value set on
132139
// the global sdk.Config (eg: `evmos`).

utils/utils_test.go

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ import (
3333
sdk "github.com/cosmos/cosmos-sdk/types"
3434
)
3535

36+
const (
37+
hex = "0x7cB61D4117AE31a12E393a1Cfa3BaC666481D02E"
38+
bech32 = "cosmos10jmp6sgh4cc6zt3e8gw05wavvejgr5pwsjskvv"
39+
)
40+
3641
func TestIsSupportedKeys(t *testing.T) {
3742
testCases := []struct {
3843
name string
@@ -86,6 +91,53 @@ func TestIsSupportedKeys(t *testing.T) {
8691
}
8792
}
8893

94+
func TestIsBech32Address(t *testing.T) {
95+
config := sdk.GetConfig()
96+
config.SetBech32PrefixForAccount("cosmos", "cosmospub")
97+
98+
testCases := []struct {
99+
name string
100+
address string
101+
expResp bool
102+
}{
103+
{
104+
"blank bech32 address",
105+
" ",
106+
false,
107+
},
108+
{
109+
"invalid bech32 address",
110+
"evmos",
111+
false,
112+
},
113+
{
114+
"invalid address bytes",
115+
"cosmos1123",
116+
false,
117+
},
118+
{
119+
"evmos address",
120+
"evmos1ltzy54ms24v590zz37r2q9hrrdcc8eslndsqwv",
121+
true,
122+
},
123+
{
124+
"cosmos address",
125+
"cosmos1qql8ag4cluz6r4dz28p3w00dnc9w8ueulg2gmc",
126+
true,
127+
},
128+
{
129+
"osmosis address",
130+
"osmo1qql8ag4cluz6r4dz28p3w00dnc9w8ueuhnecd2",
131+
true,
132+
},
133+
}
134+
135+
for _, tc := range testCases {
136+
isValid := utils.IsBech32Address(tc.address)
137+
require.Equal(t, tc.expResp, isValid, tc.name)
138+
}
139+
}
140+
89141
func TestGetAccAddressFromBech32(t *testing.T) {
90142
config := sdk.GetConfig()
91143
config.SetBech32PrefixForAccount("cosmos", "cosmospub")
@@ -116,8 +168,8 @@ func TestGetAccAddressFromBech32(t *testing.T) {
116168
},
117169
{
118170
"evmos address",
119-
"cosmos1qql8ag4cluz6r4dz28p3w00dnc9w8ueulg2gmc",
120-
"cosmos1qql8ag4cluz6r4dz28p3w00dnc9w8ueulg2gmc",
171+
"evmos1ltzy54ms24v590zz37r2q9hrrdcc8eslndsqwv",
172+
"cosmos1ltzy54ms24v590zz37r2q9hrrdcc8esl3vpw5y",
121173
false,
122174
},
123175
{
@@ -254,9 +306,6 @@ func TestAddressConversion(t *testing.T) {
254306
config := sdk.GetConfig()
255307
config.SetBech32PrefixForAccount("cosmos", "cosmospub")
256308

257-
hex := "0x7cB61D4117AE31a12E393a1Cfa3BaC666481D02E"
258-
bech32 := "cosmos10jmp6sgh4cc6zt3e8gw05wavvejgr5pwsjskvv"
259-
260309
require.Equal(t, bech32, utils.Bech32StringFromHexAddress(hex))
261310
gotAddr, err := utils.HexAddressFromBech32String(bech32)
262311
require.NoError(t, err)

0 commit comments

Comments
 (0)