Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bech32 Address Hooks #10613

Merged
merged 4 commits into from
Dec 11, 2024
Merged

feat: bech32 Address Hooks #10613

merged 4 commits into from
Dec 11, 2024

Conversation

michaelfig
Copy link
Member

@michaelfig michaelfig commented Dec 3, 2024

closes: #10614
refs: #10249 #10250

Description

Pack a bech32 "base address" (like "agoric1...") plus HTTP query string "hook parameter" bytes into a bech32 "address hook". These address hooks are bech32-encoded (i.e., all of their data is encoded and part of the bech32 checksum), but they may be up to 1024 characters in length (instead of the default 90-character length limit).

Example

import {
  encodeAddressHook,
  decodeAddressHook,
} from '@agoric/cosmic-proto/address-hooks.js';

const baseAddress = 'agoric1qqp0e5ys';
const query = { key: 'value', foo: ['bar', 'baz'] };

const addressHook = encodeAddressHook(baseAddress, query);
// 'agoric10rchqqplvehk70tzv9ezven0du7kyct6ye4k27faweskcat9qqqstnf2eq'

addressHook.startsWith('agoric10rch');
// true

const decoded = decodeAddressHook(addressHook);
// {
//   baseAddress: 'agoric1qqp0e5ys',
//   query: [Object: null prototype] { foo: [ 'bar', 'baz' ], key: 'value' }
// }

Encoding

Specifically, an address hook looks like "agoric10rch...", and its binary payload consists of:

offset 0 3 3+len(baseAddress) len(payload)-2
data magic baseAddress hookData len(baseAddress)

magic is a 3-byte prefix that identifies a hooked address and its version nibble,
whose value is 4 bits (between 0 and 0xf (15)). Currently, the only supported version is 0.

0x78, 0xf1, (0x70 | ADDRESS_HOOK_VERSION),

This magic prefix encodes as 0rch, regardless of the version or HRP (e.g.
'agoric10rch<rest of payload as bech32><bech32 checksum>').

Security Considerations

Improves robustness of address hook extraction with magic bytes and version nibble, as well as length validation.

Scaling Considerations

n/a

Documentation Considerations

Needs to be documented as part of Orch Core Address Hooks.

Testing Considerations

Should be tested as part of a regular contract. Already verified that encoding/decoding unit tests can pass in a JS compartment without any special powers.

Upgrade Considerations

Layered to facilitate the construction of arbitrary hookData, not just URL query strings. Should be extensible if needed.

Copy link

cloudflare-workers-and-pages bot commented Dec 3, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: c8ad417
Status: ✅  Deploy successful!
Preview URL: https://03d783f9.agoric-sdk.pages.dev
Branch Preview URL: https://mfig-bech32-address-hooks.agoric-sdk.pages.dev

View logs

@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch 3 times, most recently from 5255c1a to 2083b95 Compare December 8, 2024 01:29
@michaelfig michaelfig marked this pull request as ready for review December 8, 2024 01:52
@michaelfig michaelfig requested a review from a team as a code owner December 8, 2024 01:52
@michaelfig michaelfig self-assigned this Dec 8, 2024
@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch 2 times, most recently from 7f36f5d to a7fb069 Compare December 9, 2024 00:52
@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch 3 times, most recently from b85e8fc to 17cbf36 Compare December 10, 2024 17:56
@michaelfig michaelfig marked this pull request as draft December 10, 2024 18:02
@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch from 17cbf36 to 40191ad Compare December 10, 2024 18:07
@LuqiPan LuqiPan requested a review from dckc December 10, 2024 18:21
@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch from 40191ad to 77f9587 Compare December 10, 2024 18:32
@michaelfig michaelfig marked this pull request as ready for review December 10, 2024 18:32
Copy link
Member

@dckc dckc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finished reviewing the JS part, which is handy, cuz now I grok the design...

@@ -0,0 +1,231 @@
/**
* @module address-hooks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice docs. 👏

Bummer they don't show up in https://mfig-bech32-address-hooks.agoric-sdk.pages.dev/ 😢

Looks like cosmic-proto is entirely missing from our generated docs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wired up some docs now, which are better than nothing IMO. If they need much more work, I'd advocate for doing that in a new PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

* const decoded = decodeAddressHook(addressHook);
* // {
* // baseAddress: 'agoric1qqp0e5ys',
* // query: [Object: null prototype] { foo: [ 'bar', 'baz' ], key: 'value' }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supports array values. interesting.

cc @0xpatrickdev

export const DEFAULT_HOOKED_ADDRESS_CHAR_LIMIT = 1024;

/**
* @typedef {Record<string, string | (string | null)[] | null>} HookQuery
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NTS: What does null represent here, I wonder? Is this a convention from the query-string package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've documented the format in this JSDoc block... it is inherited from query-string, but I don't want to use query-string's types directly.

Comment on lines 49 to 53
// The default maximum number of characters in a bech32-encoded hooked address.
export const DEFAULT_HOOKED_ADDRESS_CHAR_LIMIT = 1024;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is API documentation, not an implementation detail comment, right?

Suggested change
// The default maximum number of characters in a bech32-encoded hooked address.
export const DEFAULT_HOOKED_ADDRESS_CHAR_LIMIT = 1024;
/** The default maximum number of characters in a bech32-encoded hooked address. */
export const DEFAULT_HOOKED_ADDRESS_CHAR_LIMIT = 1024;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re Security Considerations, I see:

Advanced
BIP173 enforces a limitation of 90 characters, if extend the LIMIT parameter beyond this, be aware that the effectiveness of checksum decreases as the length increases.

I don't suppose we're relying on a bech32 checksum for security beyond helping detect typos, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. The checksum is just to avoid typo footguns, not providing any kind of authentication.


const magicLength = ADDRESS_HOOK_MAGIC.length;
const hookBuf = new Uint8Array(
magicLength + bytes.length + hookData.length + BASE_ADDRESS_LENGTH_BYTES,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we use hookData.length without any dynamic check on the type of hookData. So joinHookedAddress is exported but not as defensive as, say, ERTP stuff.

I wonder about documentation conventions for levels of defensiveness.

I've been living in a world of exo interface guards, so reviewing this sort of thing is slightly different.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some code to check that hookData.length is a non-negative safe integer. As for verifying the rest of its structure, I'll leave that up to hookBuf.set(hookData, ...).

Comment on lines +64 to +71
test(
'roundtripEmpty',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after mulling over what this test establishes, I thought about property testing / fuzzing to be sure the encoding is unambiguous - that no 2 distinct inputs lead to the same encoded address.

But that would say that the design is flawed. And as noted above, I assume the design is agreed by the relevant parties.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely enamoured with the tests as they exist now. They're that way by attrition, less so by intention.

And they don't cover any of the error cases, just manual happy paths.

dst: ['a', 'b', 'c'],
},
'cosmos10rchqqplv3ehg0tpyej8xapavgnxgum5843jvetkv4e8jargd9hxwqqp4vx73n',
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To check default length limits against expected fast-usdc usage, let's please add a test to replace what's currently in the fast usdc test fixture: agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men

  • base address is agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek
  • query is { EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men' }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoded as agoric10rchp4vc53apxn32q42c3zryml8xq3xshyzuhjk6405wtxy7tl3d7e0f8az423padaek6me38qekget2vdhx66mtvy6kg7nrw5uhsaekd4uhwufswqex6dtsv44hxv3cd4jkuqpqvduyhf

Length: 149

We're mostly paying the 8/5 bech32 encoding tax on every byte of the query string. If we really need a smaller string, we'd have to resort to adding some compression.

Comment on lines +165 to +166
"bech32": "^2.0.0",
"query-string": "^9.1.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supply chain audit notes: I don't see any yarn.lock changes re bech32, so I'll pass on that without further remark.

query-string@^9.1.1 seems to be new. npm shows 13M weekly downloads. Clearly pretty mature stuff.

@@ -0,0 +1,214 @@
import rawTest from '@endo/ses-ava/prepare-endo.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes I wonder whether I should be using ses-ava. For my edification: what motivated you to use it here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was available from @endo, so it didn't cause dependency cycles like @agoric/swingset-vat/tools/prepare-test-env-ava.js would.

Comment on lines +16 to +20
"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"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how to read this change. What does it do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved .../x/vtransfer/types/baseaddr.go to .../types/address_hooks.go. This change gets the types.Whatever functions (whose callers were unmodified in this PR, so they don't show up in its commits) from the new Golang package (stuff in a single directory) where I moved address_hooks.go.

Copy link
Member

@dckc dckc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dealing with byte slices and such is quite a bit nicer in go than JS, isn't it :)

Before I approve, I think I should understand the change in golang/cosmos/x/vtransfer/keeper/keeper.go.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subpath... I don't remember that in the JS stuff.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a stale comment. In the new encoding, there is never a "subpath" since the baseAddr isn't actually in the same string as the query.

// Skip over parsing control flags.
ForceQuery: parsed.ForceQuery,
OmitHost: parsed.OmitHost,
bz := bytes.TrimPrefix(payload, AddressHookMagic)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bz is mnemonic for something like "bytez", a play on "bytes"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's a Golang convention I picked up from Golang json and protobuf codecs.

return "", fmt.Errorf("base address cannot be empty")
b := 0
for i := BaseAddressLengthBytes - 1; i >= 0; i -= 1 {
by := bz[len(bz)-1-i]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now we have by and bz, as if it were an x, y, z thing. But z is a slice and y is a byte. ouch. hurts my brain.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't call it byte because that's a builtin data type. I'll try byteVal.

}

b := len(bz)
if b > 0xffff {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elsewhere we act like the number of bytes in a base address length is a parameter, and we do loops and stuff. Why not hard-code 2 bytes everywhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I think this was half-finished as I rewrote the JS encoder (in my first upgrade from 1 length byte to 2 length bytes).

I want to leave it as a parameter, so I did that.

payload = append(payload, AddressHookMagic...)
payload = append(payload, bz...)
payload = append(payload, hookData...)
payload = append(payload, byte(b>>8), byte(b))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another place where BaseAddressLengthBytes has to be 2.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted.

Comment on lines 19 to +24
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"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go has nice table literals!

Comment on lines 71 to 87
cosmosHook, err := types.JoinHookedAddress("cosmos1qqxuevtt", []byte("?foo=bar&baz=bot#fragment"))
require.NoError(t, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to test only that the call does not err. Is that right?

I'm curious what happens to #fragment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested SplitHookedAddress and verified the results. That turned up a bug (wasn't stripping the baseAddr length bytes from the end of an Address Hook). Thanks!

@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch from 5023e23 to f984698 Compare December 11, 2024 00:09
@michaelfig michaelfig added automerge:no-update (expert!) Automatically merge without updates agd Agoric (Golang) Daemon labels Dec 11, 2024
Copy link
Contributor

mergify bot commented Dec 11, 2024

This pull request has been removed from the queue for the following reason: checks failed.

The merge conditions cannot be satisfied due to failing checks:

You should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it.

If you want to requeue this pull request, you need to post a comment with the text: @mergifyio requeue

@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch from f984698 to 6ff495a Compare December 11, 2024 01:42
@michaelfig michaelfig force-pushed the mfig-bech32-address-hooks branch from 6ff495a to c8ad417 Compare December 11, 2024 02:07
@mergify mergify bot merged commit 945fef1 into master Dec 11, 2024
81 checks passed
@mergify mergify bot deleted the mfig-bech32-address-hooks branch December 11, 2024 02:55
Copy link
Member

@gibson042 gibson042 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I didn't finish reviewing before this was merged, but I think it's a bad idea for these implementation to pass through hooked addresses with nonzero version nibble as if they were not hooked. Instead, such addresses should be rejected for using an unsupported version, allowing us to actually introduce such functionality in the future.

Comment on lines +54 to +58
bz := bytes.TrimPrefix(payload, AddressHookMagic)
if len(bz) == len(payload) {
// Return an unhooked address.
return addr, []byte{}, nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For forward compatibility, this should probably error upon encountering a hooked address with an unknown version.

Suggested change
bz := bytes.TrimPrefix(payload, AddressHookMagic)
if len(bz) == len(payload) {
// Return an unhooked address.
return addr, []byte{}, nil
}
bz := bytes.TrimPrefix(payload, AddressHookMagic[:2])
if len(bz) == len(payload) || len(bz) == 0 || bz[0] & 0x70 != 0x70 {
// Return an unhooked address.
return addr, []byte{}, nil
}
version := bz[0] & 0x0F
bz = bz[1:]
if version != AddressHookVersion {
return "", []byte{}, fmt.Errorf("unsupported version %d", version)
}

(and likewise in the JS version)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@turadg , @LuqiPan ,

I suppose this shows that my assumption that the design was agreed by all the relevant parties is not correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
agd Agoric (Golang) Daemon automerge:no-update (expert!) Automatically merge without updates
Projects
None yet
Development

Successfully merging this pull request may close these issues.

rework EUD address hook to use bech32
3 participants