Skip to content

Commit

Permalink
feat(vtransfer): port some address-hooks.js functions to Go
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Dec 10, 2024
1 parent bee982b commit 40191ad
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,20 @@ 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) {
bases := []struct {
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
}{
Expand All @@ -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)
}
})
}
}
})
}
}
}
Expand All @@ -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"},
},
},
}
Expand Down
5 changes: 4 additions & 1 deletion golang/cosmos/x/vtransfer/ibc_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`,
)

Expand Down
3 changes: 2 additions & 1 deletion golang/cosmos/x/vtransfer/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 40191ad

Please sign in to comment.