Skip to content

Instantly share code, notes, and snippets.

@0xpatrickdev
Last active April 12, 2025 18:48
Show Gist options
  • Save 0xpatrickdev/b2c873b268cbc037d98ae880c9923f1b to your computer and use it in GitHub Desktop.
Save 0xpatrickdev/b2c873b268cbc037d98ae880c9923f1b to your computer and use it in GitHub Desktop.
bech32-combined-addresses

two-way

produces a long address, but derivation works both ways

go run ./two-way
Test Case 1:
LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Receiver Address: osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men
Chain ID: osmosis-1
Combined address: agoric1qyrxzem0wf5kxgx4nzj85y6w9gz4tzygvn0uuczy6zustj72m2473evcne079hm9ayzx7umddu2rckuevfamtw6x3vwzn8ddj8q8s4d6qumqjmmnd4hhx6tn95cszjzqwh
Decoded LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Decoded Receiver Address: osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men
Decoded Chain ID: osmosis-1
Original LCA Address matches decoded: true
Original Receiver Address matches decoded: true
Original Chain ID matches decoded: true
Length of combined address: 137 characters

Test Case 2:
LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Receiver Address: osmo1n4m7amq25q9wh4htusw3lm8g8nau6m4qtllzxedf22dqpjpge9mqpkuve0
Chain ID: osmosis-2
Combined address: agoric1qyrxzem0wf5kxgx4nzj85y6w9gz4tzygvn0uuczy6zustj72m2473evcne079hm9ayzx7umddusf6alwas92qzht6m47g8glan5re77dd6s9ll3rvk549xsqeq5vjasfdaek6mmnd9ej6vstrj944
Decoded LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Decoded Receiver Address: osmo1n4m7amq25q9wh4htusw3lm8g8nau6m4qtllzxedf22dqpjpge9mqpkuve0
Decoded Chain ID: osmosis-2
Original LCA Address matches decoded: true
Original Receiver Address matches decoded: true
Original Chain ID matches decoded: true
Length of combined address: 156 characters

one-way

uses cosmos-sdk's built in address.Derive(), but seems to require a lookup table / storage

go run ./one-way
Test Case 1:
LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Receiver Address: osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men
Chain ID: osmosis-1
Generated address: agoric1ya5akgcqw97rvnhfu8jzapp8rfwk4tddkpwsmz
Encoded data: YWdvcmljMTZrdjJnN3NuZmM0cTI0dmczcGpkbG5ucWduZ3RqcHd0ZXRkMmg2ODluejA5bGNrbHZoNXM4dTM3ZWsrb3NtbzE4M2RlamNubWtrYTVkemN1OXh3Nm15d3EwcDJtNXBla3MyOG1lbitvc21vc2lzLTE=
Decoded LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Decoded Receiver Address: osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men
Decoded Chain ID: osmosis-1
Original LCA Address matches decoded: true
Original Receiver Address matches decoded: true
Original Chain ID matches decoded: true
Length of generated address: 45 characters
Generated address has correct length of 20 bytes
Address generation is deterministic: true

Test Case 2:
LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Receiver Address: osmo1n4m7amq25q9wh4htusw3lm8g8nau6m4qtllzxedf22dqpjpge9mqpkuve0
Chain ID: osmosis-2
Generated address: agoric1rky8rtq8znh65m7e2283w766kv8676qwlegcfj
Encoded data: YWdvcmljMTZrdjJnN3NuZmM0cTI0dmczcGpkbG5ucWduZ3RqcHd0ZXRkMmg2ODluejA5bGNrbHZoNXM4dTM3ZWsrb3NtbzFuNG03YW1xMjVxOXdoNGh0dXN3M2xtOGc4bmF1Nm00cXRsbHp4ZWRmMjJkcXBqcGdlOW1xcGt1dmUwK29zbW9zaXMtMg==
Decoded LCA Address: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
Decoded Receiver Address: osmo1n4m7amq25q9wh4htusw3lm8g8nau6m4qtllzxedf22dqpjpge9mqpkuve0
Decoded Chain ID: osmosis-2
Original LCA Address matches decoded: true
Original Receiver Address matches decoded: true
Original Chain ID matches decoded: true
Length of generated address: 45 characters
Generated address has correct length of 20 bytes
Address generation is deterministic: true
module combined-address
go 1.22.8
require github.com/cosmos/cosmos-sdk v0.46.16
require (
cosmossdk.io/errors v1.0.0-beta.7 // indirect
cosmossdk.io/math v1.0.0-rc.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/confio/ics23/go v0.9.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-proto v1.0.0-alpha7 // indirect
github.com/cosmos/gorocksdb v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-kit/kit v0.12.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.13.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tendermint/tendermint v0.34.29 // indirect
github.com/tendermint/tm-db v0.6.7 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace (
github.com/cosmos/cosmos-sdk => github.com/agoric-labs/cosmos-sdk v0.46.16-alpha.agoric.2.4
github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
github.com/tendermint/tendermint => github.com/agoric-labs/cometbft v0.34.30-alpha.agoric.1
)
package main
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/types/address"
"github.com/cosmos/cosmos-sdk/types/bech32"
)
const (
AddressPrefix = "agoric"
ModuleName = "forwarding"
separator = "+"
)
var (
ErrInvalidEncodedData = errors.New("invalid encoded data")
)
type AddressData struct {
LCAAddress string
ReceiverAddress string
ChainID string
}
func GenerateAddress(lcaAddr, receiverAddr, chainID string) (string, error) {
bz := []byte(lcaAddr + receiverAddr + chainID)
addr := address.Derive([]byte(ModuleName), bz)[12:]
return bech32.ConvertAndEncode(AddressPrefix, addr)
}
func EncodeOriginalData(lcaAddr, receiverAddr, chainID string) string {
data := strings.Join([]string{lcaAddr, receiverAddr, chainID}, separator)
return base64.StdEncoding.EncodeToString([]byte(data))
}
func DecodeOriginalData(encodedData string) (*AddressData, error) {
decodedBytes, err := base64.StdEncoding.DecodeString(encodedData)
if err != nil {
return nil, err
}
parts := strings.Split(string(decodedBytes), separator)
if len(parts) != 3 {
return nil, ErrInvalidEncodedData
}
return &AddressData{
LCAAddress: parts[0],
ReceiverAddress: parts[1],
ChainID: parts[2],
}, nil
}
func main() {
testCases := []struct {
lcaAddr string
receiverAddr string
chainID string
}{
{
lcaAddr: "agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek",
receiverAddr: "osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men",
chainID: "osmosis-1",
},
{
lcaAddr: "agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek",
receiverAddr: "osmo1n4m7amq25q9wh4htusw3lm8g8nau6m4qtllzxedf22dqpjpge9mqpkuve0",
chainID: "osmosis-2",
},
}
for i, tc := range testCases {
fmt.Printf("Test Case %d:\n", i+1)
runTestCase(tc.lcaAddr, tc.receiverAddr, tc.chainID)
fmt.Println()
}
}
func runTestCase(lcaAddr, receiverAddr, chainID string) {
fmt.Printf("LCA Address: %s\n", lcaAddr)
fmt.Printf("Receiver Address: %s\n", receiverAddr)
fmt.Printf("Chain ID: %s\n", chainID)
address, err := GenerateAddress(lcaAddr, receiverAddr, chainID)
if err != nil {
fmt.Printf("Error generating address: %v\n", err)
return
}
fmt.Printf("Generated address: %s\n", address)
encodedData := EncodeOriginalData(lcaAddr, receiverAddr, chainID)
fmt.Printf("Encoded data: %s\n", encodedData)
decoded, err := DecodeOriginalData(encodedData)
if err != nil {
fmt.Printf("Error decoding data: %v\n", err)
return
}
fmt.Printf("Decoded LCA Address: %s\n", decoded.LCAAddress)
fmt.Printf("Decoded Receiver Address: %s\n", decoded.ReceiverAddress)
fmt.Printf("Decoded Chain ID: %s\n", decoded.ChainID)
fmt.Printf("Original LCA Address matches decoded: %v\n", lcaAddr == decoded.LCAAddress)
fmt.Printf("Original Receiver Address matches decoded: %v\n", receiverAddr == decoded.ReceiverAddress)
fmt.Printf("Original Chain ID matches decoded: %v\n", chainID == decoded.ChainID)
fmt.Printf("Length of generated address: %d characters\n", len(address))
_, decodedAddr, err := bech32.DecodeAndConvert(address)
if err != nil {
fmt.Printf("Error decoding generated address: %v\n", err)
return
}
if len(decodedAddr) != 20 {
fmt.Printf("Generated address has incorrect length: got %d, want 20\n", len(decodedAddr))
} else {
fmt.Println("Generated address has correct length of 20 bytes")
}
}
package main
import (
"errors"
"fmt"
"github.com/cosmos/cosmos-sdk/types/bech32"
)
const (
agoricPrefix = "agoric"
separator = byte(1)
)
var (
ErrDecodingAddress = errors.New("error decoding address")
ErrEncodingAddress = errors.New("error encoding address")
ErrInvalidCombinedData = errors.New("invalid combined address data")
)
// CombinedAddressData represents the decoded data from a combined address
type CombinedAddressData struct {
LCAAddress string
ReceiverAddress string
ChainID string
}
func encodeCombinedAddress(lcaAddr, receiverAddr, chainID string) (string, error) {
// Decode both addresses
prefix1, data1, err := bech32.DecodeAndConvert(lcaAddr)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDecodingAddress, err)
}
prefix2, data2, err := bech32.DecodeAndConvert(receiverAddr)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDecodingAddress, err)
}
// Combine all information
combined := []byte{separator}
combined = appendPrefixedData(combined, []byte(prefix1), data1)
combined = appendPrefixedData(combined, []byte(prefix2), data2)
combined = appendPrefixedData(combined, []byte(chainID), nil) // Chain ID has no associated data
return bech32.ConvertAndEncode(agoricPrefix, combined)
}
// appendPrefixedData appends length-prefixed data to the destination slice
func appendPrefixedData(dst, prefix, data []byte) []byte {
dst = append(dst, byte(len(prefix)))
dst = append(dst, prefix...)
if data != nil {
dst = append(dst, byte(len(data)))
dst = append(dst, data...)
}
return dst
}
func decodeCombinedAddress(combinedAddress string) (*CombinedAddressData, error) {
_, data, err := bech32.DecodeAndConvert(combinedAddress)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrDecodingAddress, err)
}
if len(data) == 0 || data[0] != separator {
return nil, ErrInvalidCombinedData
}
data = data[1:] // Remove separator byte
return extractAddressesAndChainID(data)
}
func extractAddressesAndChainID(data []byte) (*CombinedAddressData, error) {
result := &CombinedAddressData{}
var err error
// Extract first address
data, result.LCAAddress, err = extractNextItem(data)
if err != nil {
return nil, err
}
// Extract second address
data, result.ReceiverAddress, err = extractNextItem(data)
if err != nil {
return nil, err
}
// Extract Chain ID
if len(data) == 0 {
return nil, ErrInvalidCombinedData
}
chainIDLen := int(data[0])
if len(data) < chainIDLen+1 {
return nil, ErrInvalidCombinedData
}
result.ChainID = string(data[1 : chainIDLen+1])
return result, nil
}
func extractNextItem(data []byte) ([]byte, string, error) {
if len(data) < 2 {
return nil, "", ErrInvalidCombinedData
}
prefixLen := int(data[0])
data = data[1:]
if len(data) < prefixLen+1 {
return nil, "", ErrInvalidCombinedData
}
prefix := string(data[:prefixLen])
data = data[prefixLen:]
addressLen := int(data[0])
data = data[1:]
if len(data) < addressLen {
return nil, "", ErrInvalidCombinedData
}
addressData := data[:addressLen]
data = data[addressLen:]
encodedAddr, err := bech32.ConvertAndEncode(prefix, addressData)
if err != nil {
return nil, "", fmt.Errorf("%w: %v", ErrEncodingAddress, err)
}
return data, encodedAddr, nil
}
func main() {
testCases := []struct {
lcaAddr string
receiverAddr string
chainID string
}{
{
lcaAddr: "agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek",
receiverAddr: "osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men", // BaseAccount
chainID: "osmosis-1",
},
{
lcaAddr: "agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek",
receiverAddr: "osmo1n4m7amq25q9wh4htusw3lm8g8nau6m4qtllzxedf22dqpjpge9mqpkuve0", // ModuleAccount (ICA)
chainID: "osmosis-2",
},
}
for i, tc := range testCases {
fmt.Printf("Test Case %d:\n", i+1)
runTestCase(tc.lcaAddr, tc.receiverAddr, tc.chainID)
fmt.Println()
}
}
func runTestCase(lcaAddr, receiverAddr, chainID string) {
fmt.Printf("LCA Address: %s\n", lcaAddr)
fmt.Printf("Receiver Address: %s\n", receiverAddr)
fmt.Printf("Chain ID: %s\n", chainID)
combined, err := encodeCombinedAddress(lcaAddr, receiverAddr, chainID)
if err != nil {
fmt.Printf("Error encoding combined address: %v\n", err)
return
}
fmt.Printf("Combined address: %s\n", combined)
decoded, err := decodeCombinedAddress(combined)
if err != nil {
fmt.Printf("Error decoding combined address: %v\n", err)
return
}
fmt.Printf("Decoded LCA Address: %s\n", decoded.LCAAddress)
fmt.Printf("Decoded Receiver Address: %s\n", decoded.ReceiverAddress)
fmt.Printf("Decoded Chain ID: %s\n", decoded.ChainID)
fmt.Printf("Original LCA Address matches decoded: %v\n", lcaAddr == decoded.LCAAddress)
fmt.Printf("Original Receiver Address matches decoded: %v\n", receiverAddr == decoded.ReceiverAddress)
fmt.Printf("Original Chain ID matches decoded: %v\n", chainID == decoded.ChainID)
fmt.Printf("Length of combined address: %d characters\n", len(combined))
}
@0xpatrickdev
Copy link
Author

nobled query forwarding address channel-99 agoric1qyrxzem0wf5kxgx4nzj85y6w9gz4tzygvn0uuczy6zustj72m2473evcne079hm9ayzx7umddu2rckuevfamtw6x3vwzn8ddj8q8s4d6qumqjmmnd4hhx6tn95cszjzqwh --node https://noble-testnet-rpc.polkachu.com:443
address: noble1kn8aezm02u7h6utxzzjeyuk4wletxuquhdgh5r
exists: false
nobled query forwarding address channel-99 agoric1qyrxzem0wf5kxgx4nzj85y6w9gz4tzygvn0uuczy6zustj72m2473evcne079hm9ayzx7umddusf6alwas92qzht6m47g8glan5re77dd6s9ll3rvk549xsqeq5vjasfdaek6mmnd9ej6vstrj944 --node https://noble-testnet-rpc.polkachu.com:443
address: noble1nqnfxwx6e0jhytgq2qqngu29xwvtsjekx9yyt5
exists: false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment