-
Notifications
You must be signed in to change notification settings - Fork 214
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(vtransfer): extract base address from parameterized address #10250
Conversation
98f0cc8
to
792b993
Compare
Deploying agoric-sdk with Cloudflare Pages
|
80cdb7b
to
63041aa
Compare
de6e865
to
217d5f3
Compare
+
-separated virtual targets2b3b4a3
to
62d0488
Compare
62d0488
to
97b28f5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed the code in keeper.go
, nor am I an expert in reviewing that. But I did a quick review of the address parsing logic
// Extract the base address from the packet sender (if senderIsLocal) or | ||
// receiver (if !senderIsLocal), since the local ibcModule doesn't understand | ||
// address parameters. | ||
func (k Keeper) packetWithOnlyBaseAddresses(packet channeltypes.Packet, senderIsLocal bool) channeltypes.Packet { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question
Could we add some unit tests for this function? If nothing else, just to use unit tests to document the behavior of this function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely! I'll work on those.
RawQuery: parsed.RawQuery, | ||
Fragment: parsed.Fragment, | ||
RawFragment: parsed.RawFragment, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
url.URL
has more fields than being populated here. We're relying on the other fields like parsed.OmitHost
or parsed.ForceQuery
to be false
, which is the default value for the comparison at line 31 to be comparing what we truly care about.
IMO doing
if ((parsed.Opaque == trimSlashPrefix(parsed.Opaque) && ...)
is still very readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm relying on all of the non-allowlisted fields to be empty, otherwise to fail. That was the most concise and robust way of ensuring the parsing worked correctly. I had originally done an if-ladder and found it to be much less readable than the declaration of expected
and the single comparison of parsed
to expected
.
However, I'll wait for some other opinions on this, and keep it or change it based on the group's agreement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this approach and in my inherited Go practice, relying on the zero values for a struct is idiomatic. This is robust against evolution of the struct in a way that an if-ladder would not be, since we want the presence of non-zero values in future fields to result in errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @kriskowal. I also rely on Go zero values, I vote for this approach as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LuqiPan were you pointing out that OmitHost
and ForceQuery
shouldn't have to be the zero-value, since they are parsing options? I just now carved out an exception for both of these two fields.
// Replace the packet's data with the new data. | ||
packet.Data = data | ||
return packet | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it expected behavior that when there is an error, we send an empty JSON on Data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! No, not expected. I'll fix this while working on its unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The general approach seems good, but I think it could use some tweaks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refactoring really works. A few style suggestions, but otherwise LGTM.
// ExtractBaseTransferData returns the base address from the transferData.Sender | ||
// (if RoleSender) or transferData.Receiver (if RoleReceiver). Errors in | ||
// determining the base address are ignored... we then assume the base address | ||
// is exactly the original address. If newTransferData is not nil, it will be | ||
// populated with a new FungibleTokenPacketData consisting of the role replaced | ||
// with its base address. | ||
func ExtractBaseTransferData(transferData transfertypes.FungibleTokenPacketData, role AddressRole, newTransferData *transfertypes.FungibleTokenPacketData) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exporting this function fosters unnecessary confusion w.r.t. ExtractBaseTransferPacket
IMO.
// ExtractBaseTransferData returns the base address from the transferData.Sender | |
// (if RoleSender) or transferData.Receiver (if RoleReceiver). Errors in | |
// determining the base address are ignored... we then assume the base address | |
// is exactly the original address. If newTransferData is not nil, it will be | |
// populated with a new FungibleTokenPacketData consisting of the role replaced | |
// with its base address. | |
func ExtractBaseTransferData(transferData transfertypes.FungibleTokenPacketData, role AddressRole, newTransferData *transfertypes.FungibleTokenPacketData) (string, error) { | |
// extractBaseTransferData returns the base address from the transferData.Sender | |
// (if RoleSender) or transferData.Receiver (if RoleReceiver). Errors in | |
// determining the base address are ignored... we then assume the base address | |
// is exactly the original address. If newTransferData is not nil, it will be | |
// populated with a new FungibleTokenPacketData consisting of the role replaced | |
// with its base address. | |
func extractBaseTransferData(transferData transfertypes.FungibleTokenPacketData, role AddressRole, newTransferData *transfertypes.FungibleTokenPacketData) (string, error) { |
// ExtractBaseTransferPacket returns the base address from the transfer packet's | ||
// transferData.Sender (if RoleSender) or transferData.Receiver (if | ||
// RoleReceiver). If newPacket is not nil, it is populated with a new transfer | ||
// packet whose corresponding Sender/Receiver is replaced with its base address. | ||
func ExtractBaseTransferPacket(cdc codec.Codec, packet ibcexported.PacketI, role AddressRole, newPacket *channeltypes.Packet) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested rewording and renaming:
// ExtractBaseTransferPacket returns the base address from the transfer packet's | |
// transferData.Sender (if RoleSender) or transferData.Receiver (if | |
// RoleReceiver). If newPacket is not nil, it is populated with a new transfer | |
// packet whose corresponding Sender/Receiver is replaced with its base address. | |
func ExtractBaseTransferPacket(cdc codec.Codec, packet ibcexported.PacketI, role AddressRole, newPacket *channeltypes.Packet) (string, error) { | |
// ExtractBaseAddressFromPacket returns the base address from a transfer | |
// packet's data, either Sender (if role is RoleSender) or Receiver (if role is | |
// RoleReceiver). | |
// Errors in determining the base address are ignored... we then assume the base | |
// address is exactly the original address. | |
// If newPacket is not nil, it is populated with a new transfer packet whose | |
// corresponding Sender or Receiver is replaced with the extracted base address. | |
func ExtractBaseAddressFromPacket(cdc codec.Codec, packet ibcexported.PacketI, role AddressRole, newPacket *channeltypes.Packet) (string, error) { |
// parseTransfer checks if a packet's sender and/or receiver are targeted accounts. | ||
func (k Keeper) parseTransfer(ctx sdk.Context, packet ibcexported.PacketI) (string, string, error) { | ||
var transferData transfertypes.FungibleTokenPacketData | ||
err := k.cdc.UnmarshalJSON(packet.GetData(), &transferData) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
var sender string | ||
var receiver string | ||
// targetIsWatched checks if a target address has been watched by the VM. | ||
func (k Keeper) targetIsWatched(ctx sdk.Context, target string) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is such a good change. 👍
6373538
to
3d44b53
Compare
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 ```js 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. ```js 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.
closes: #10249
Description
This PR allows ICS-20 fungible token transfer packets to contain inbound receiver and outbound sender addresses formatted as either:
"agoric1bech32addr"
), or"agoric1bech32addr/opt/account?k=v&k2=v2"
)In these cases, the "base address" is
"agoric1bech32addr"
, and tokens are transferred to that base address. The original address can be parsed by a listening contract into "address parameters" to use at its discretion.Security Considerations
Should be no impact. If a transfer listener does not explicitly look for the data in an inbound packet, it will behave as if there are no address parameters.
Careful review of the code is critical, though, since it touches the powerful
x/vtransfer
module.Scaling Considerations
Increases the scalability of contracts that would ordinarily have to maintain a pool of multiple Local Chain Account (LCA) addresses to have a unique ID to map to some parameters. Now, contracts can create just one LCA and have the sender supply the data as part of a parameterized address.
The ICS-20 transfer addresses are strings limited to at most 2048 bytes, and potentially even smaller if explicitly configured by a relayer operator. Thus, the total length of the encoded base address and all the address parameters cannot exceed that address string limit, or the packet will not be forwarded.
Documentation Considerations
Should be documented as a feature of
vtransfer
.Testing Considerations
Unit tests with address parameter data tests have been implemented, testing both the address parser code and the IBC end-to-end packet relay process through the
x/vtransfer
module.Upgrade Considerations
This is a chain-level, backwards-compatible change, so no upgrade aspects must be addressed. However, Agoric contracts will probably need to import an URL-parsing module to parse URL escaping and query parameters syntax (since the
globalThis.URL
constructor is not currently available in the SwingSet vat environment).