diff --git a/golang/cosmos/x/vtransfer/types/baseaddr.go b/golang/cosmos/types/address_hooks.go similarity index 60% rename from golang/cosmos/x/vtransfer/types/baseaddr.go rename to golang/cosmos/types/address_hooks.go index 7ad65541746..4c4fcfd0803 100644 --- a/golang/cosmos/x/vtransfer/types/baseaddr.go +++ b/golang/cosmos/types/address_hooks.go @@ -1,11 +1,11 @@ package types import ( + "bytes" "fmt" - "net/url" - "strings" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/bech32" transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" @@ -18,48 +18,90 @@ type AddressRole string const ( RoleSender AddressRole = "Sender" RoleReceiver AddressRole = "Receiver" + + AddressHookVersion = 0 + BaseAddressLengthBytes = 2 ) -func trimSlashPrefix(s string) string { - return strings.TrimPrefix(s, "/") +// AddressHookMagic is a magic byte prefix that identifies a hooked address. +// Chosen to make bech32 address hooks that look like "agoric10rch..." +var AddressHookMagic = []byte{0x78, 0xf1, 0x70 | AddressHookVersion} + +func init() { + if AddressHookVersion&0x0f != AddressHookVersion { + panic(fmt.Sprintf("AddressHookVersion must be less than 0x10, got 0x%x", AddressHookVersion)) + } } // ExtractBaseAddress extracts the base address from a parameterized address. // It removes all subpath and query components from addr. func ExtractBaseAddress(addr string) (string, error) { - parsed, err := url.Parse(addr) + baseAddr, _, err := SplitHookedAddress(addr) if err != nil { return "", err } + return baseAddr, nil +} - // Specify the fields and values we expect. Unspecified fields will only - // match if they are zero values in order to be robust against extensions to - // the url.URL struct. - // - // Remove leading slashes from the path fields so that only parsed relative - // paths match the expected test. - expected := url.URL{ - Path: trimSlashPrefix(parsed.Path), - RawPath: trimSlashPrefix(parsed.RawPath), - RawQuery: parsed.RawQuery, - Fragment: parsed.Fragment, - RawFragment: parsed.RawFragment, +// SplitHookedAddress splits a hooked address into its base address and hook data. +// For the JS implementation, look at @agoric/cosmic-proto/src/address-hooks.js. +func SplitHookedAddress(addr string) (string, []byte, error) { + prefix, payload, err := bech32.DecodeAndConvert(addr) + if err != nil { + return "", []byte{}, err + } - // Skip over parsing control flags. - ForceQuery: parsed.ForceQuery, - OmitHost: parsed.OmitHost, + bz := bytes.TrimPrefix(payload, AddressHookMagic) + if len(bz) == len(payload) { + // Return an unhooked address. + return addr, []byte{}, nil } - if *parsed != expected { - return "", fmt.Errorf("address must be relative path with optional query and fragment, got %s", addr) + if len(bz) < BaseAddressLengthBytes { + return "", []byte{}, fmt.Errorf("hooked address must have at least %d bytes", BaseAddressLengthBytes) } - baseAddr, _, _ := strings.Cut(expected.Path, "/") - if baseAddr == "" { - return "", fmt.Errorf("base address cannot be empty") + b := 0 + for i := BaseAddressLengthBytes - 1; i >= 0; i -= 1 { + by := bz[len(bz)-1-i] + b <<= 8 + b |= int(by) } - return baseAddr, nil + if b > len(bz)-BaseAddressLengthBytes { + return "", []byte{}, fmt.Errorf("base address length 0x%x is longer than specimen length 0x%x", b, len(bz)-BaseAddressLengthBytes) + } + + baseAddressBuf := bz[0:b] + baseAddress, err := bech32.ConvertAndEncode(prefix, baseAddressBuf) + if err != nil { + return "", []byte{}, err + } + + return baseAddress, bz[b:], nil +} + +// JoinHookedAddress joins a base bech32 address with hook data to create a +// hooked bech32 address. +// For the JS implementation, look at @agoric/cosmic-proto/src/address-hooks.js +func JoinHookedAddress(baseAddr string, hookData []byte) (string, error) { + prefix, bz, err := bech32.DecodeAndConvert(baseAddr) + if err != nil { + return "", err + } + + b := len(bz) + if b > 0xffff { + return "", fmt.Errorf("base address length 0x%x is longer than the maximum 0x%x", b, 1<<(8*BaseAddressLengthBytes-1)+1) + } + + payload := make([]byte, 0, len(AddressHookMagic)+b+len(hookData)+BaseAddressLengthBytes) + payload = append(payload, AddressHookMagic...) + payload = append(payload, bz...) + payload = append(payload, hookData...) + payload = append(payload, byte(b>>8), byte(b)) + + return bech32.ConvertAndEncode(prefix, payload) } // extractBaseTransferData returns the base address from the transferData.Sender diff --git a/golang/cosmos/x/vtransfer/types/baseaddr_test.go b/golang/cosmos/types/address_hooks_test.go similarity index 65% rename from golang/cosmos/x/vtransfer/types/baseaddr_test.go rename to golang/cosmos/types/address_hooks_test.go index eb83f07ddb6..e20e1540811 100644 --- a/golang/cosmos/x/vtransfer/types/baseaddr_test.go +++ b/golang/cosmos/types/address_hooks_test.go @@ -12,7 +12,7 @@ import ( clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" - "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/types" + "github.com/Agoric/agoric-sdk/golang/cosmos/types" ) func TestExtractBaseAddress(t *testing.T) { @@ -20,28 +20,12 @@ func TestExtractBaseAddress(t *testing.T) { name string addr string }{ - {"agoric address", "agoric1abcdefghiteaneas"}, - {"cosmos address", "cosmos1abcdeffiharceuht"}, - {"hex address", "0xabcdef198189818c93839ibia"}, - } - - prefixes := []struct { - prefix string - baseIsWrong bool - isErr bool - }{ - {"", false, false}, - {"/", false, true}, - {"orch:/", false, true}, - {"unexpected", true, false}, - {"norch:/", false, true}, - {"orch:", false, true}, - {"norch:", false, true}, - {"\x01", false, true}, + {"agoric address", "agoric1qqp0e5ys"}, + {"cosmos address", "cosmos1qqxuevtt"}, } suffixes := []struct { - suffix string + hookStr string baseIsWrong bool isErr bool }{ @@ -50,31 +34,29 @@ func TestExtractBaseAddress(t *testing.T) { {"/sub/account", false, false}, {"?query=something&k=v&k2=v2", false, false}, {"?query=something&k=v&k2=v2#fragment", false, false}, - {"unexpected", true, false}, - {"\x01", false, true}, + {"unexpected", false, false}, + {"\x01", false, false}, } for _, b := range bases { b := b - for _, p := range prefixes { - p := p - for _, s := range suffixes { - s := s - t.Run(b.name+" "+p.prefix+" "+s.suffix, func(t *testing.T) { - addr := p.prefix + b.addr + s.suffix - addr, err := types.ExtractBaseAddress(addr) - if p.isErr || s.isErr { - require.Error(t, err) + for _, s := range suffixes { + s := s + t.Run(b.name+" "+s.hookStr, func(t *testing.T) { + addrHook, err := types.JoinHookedAddress(b.addr, []byte(s.hookStr)) + require.NoError(t, err) + addr, err := types.ExtractBaseAddress(addrHook) + if s.isErr { + require.Error(t, err) + } else { + require.NoError(t, err) + if s.baseIsWrong { + require.NotEqual(t, b.addr, addr) } else { - require.NoError(t, err) - if p.baseIsWrong || s.baseIsWrong { - require.NotEqual(t, b.addr, addr) - } else { - require.Equal(t, b.addr, addr) - } + require.Equal(t, b.addr, addr) } - }) - } + } + }) } } } @@ -86,32 +68,37 @@ func TestExtractBaseAddressFromPacket(t *testing.T) { channeltypes.RegisterInterfaces(ir) clienttypes.RegisterInterfaces(ir) + cosmosHook, err := types.JoinHookedAddress("cosmos1qqxuevtt", []byte("?foo=bar&baz=bot#fragment")) + require.NoError(t, err) + agoricHook, err := types.JoinHookedAddress("agoric1qqp0e5ys", []byte("?bingo=again")) + require.NoError(t, err) + cases := []struct { name string addrs map[types.AddressRole]struct{ addr, baseAddr string } }{ {"sender has params", map[types.AddressRole]struct{ addr, baseAddr string }{ - types.RoleSender: {"cosmos1abcdeffiharceuht?foo=bar&baz=bot#fragment", "cosmos1abcdeffiharceuht"}, - types.RoleReceiver: {"agoric1abcdefghiteaneas", "agoric1abcdefghiteaneas"}, + types.RoleSender: {cosmosHook, "cosmos1qqxuevtt"}, + types.RoleReceiver: {"agoric1qqp0e5ys", "agoric1qqp0e5ys"}, }, }, {"receiver has params", map[types.AddressRole]struct{ addr, baseAddr string }{ - types.RoleSender: {"cosmos1abcdeffiharceuht", "cosmos1abcdeffiharceuht"}, - types.RoleReceiver: {"agoric1abcdefghiteaneas?bingo=again", "agoric1abcdefghiteaneas"}, + types.RoleSender: {"cosmos1qqxuevtt", "cosmos1qqxuevtt"}, + types.RoleReceiver: {agoricHook, "agoric1qqp0e5ys"}, }, }, {"both are base", map[types.AddressRole]struct{ addr, baseAddr string }{ - types.RoleSender: {"cosmos1abcdeffiharceuht", "cosmos1abcdeffiharceuht"}, - types.RoleReceiver: {"agoric1abcdefghiteaneas", "agoric1abcdefghiteaneas"}, + types.RoleSender: {"cosmos1qqxuevtt", "cosmos1qqxuevtt"}, + types.RoleReceiver: {"agoric1qqp0e5ys", "agoric1qqp0e5ys"}, }, }, {"both have params", map[types.AddressRole]struct{ addr, baseAddr string }{ - types.RoleSender: {"agoric1abcdefghiteaneas?bingo=again", "agoric1abcdefghiteaneas"}, - types.RoleReceiver: {"cosmos1abcdeffiharceuht?foo=bar&baz=bot#fragment", "cosmos1abcdeffiharceuht"}, + types.RoleSender: {agoricHook, "agoric1qqp0e5ys"}, + types.RoleReceiver: {cosmosHook, "cosmos1qqxuevtt"}, }, }, } diff --git a/golang/cosmos/x/vtransfer/ibc_middleware_test.go b/golang/cosmos/x/vtransfer/ibc_middleware_test.go index 23279bf8a63..ee9c80ebb56 100644 --- a/golang/cosmos/x/vtransfer/ibc_middleware_test.go +++ b/golang/cosmos/x/vtransfer/ibc_middleware_test.go @@ -16,6 +16,7 @@ import ( "github.com/tendermint/tendermint/libs/log" dbm "github.com/tendermint/tm-db" + "github.com/Agoric/agoric-sdk/golang/cosmos/types" swingsettesting "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/testing" swingsettypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/types" vibckeeper "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc/keeper" @@ -332,11 +333,13 @@ func (s *IntegrationTestSuite) TestTransferFromAgdToAgd() { s.Run("TransferFromAgdToAgd", func() { // create a transfer packet's data contents baseReceiver := s.chainB.SenderAccounts[1].SenderAccount.GetAddress().String() + receiverHook, err := types.JoinHookedAddress(baseReceiver, []byte("?what=arbitrary-data&why=to-test-bridge-targets")) + s.Require().NoError(err) transferData := ibctransfertypes.NewFungibleTokenPacketData( "uosmo", "1000000", s.chainA.SenderAccount.GetAddress().String(), - baseReceiver+"?what=arbitrary-data&why=to-test-bridge-targets", + receiverHook, `"This is a JSON memo"`, ) diff --git a/golang/cosmos/x/vtransfer/keeper/keeper.go b/golang/cosmos/x/vtransfer/keeper/keeper.go index 0d99840c061..ed5b0f57cba 100644 --- a/golang/cosmos/x/vtransfer/keeper/keeper.go +++ b/golang/cosmos/x/vtransfer/keeper/keeper.go @@ -13,10 +13,11 @@ import ( capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/Agoric/agoric-sdk/golang/cosmos/types" "github.com/Agoric/agoric-sdk/golang/cosmos/vm" "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc" vibctypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc/types" - "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/types" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" host "github.com/cosmos/ibc-go/v6/modules/core/24-host"