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

Add a builtin for decoding TokenMessage for hyperlane SPI #1344

Merged
merged 20 commits into from
Feb 22, 2024

Conversation

imalsogreg
Copy link
Contributor

@imalsogreg imalsogreg commented Feb 21, 2024

Part of the SPI interface is a TokenMessage: a tuple of a recipient, and amount, and a chain id:

{
  amount: Decimal,
  chainId: Text,
  recipient: Guard
}

The wire format for a TokenMessage is a packed binary encoding of these fields, followed by base64url encoding.

Pact needs to provide a builtin for decoding such an encoded message into its original form, to support the larger SPI scheme. This PR implements that builtin, named hyperlane-decode-token-message. It also provides a convenience function for encoding a TokenMessage, useful for testing.

Usage:

pact>   (hyperlane-decode-token-message "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGF7InByZWQiOiAia2V5cy1hbGwiLCAia2V5cyI6WyJkYTFhMzM5YmQ4MmQyYzJlOTE4MDYyNmEwMGRjMDQzMjc1ZGViM2FiYWJiMjdiNTczOGFiZjZiOWRjZWU4ZGI2Il19AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")
{"amount": 0.000000000000000123
,"chainId": "4"
,"recipient": KeySet {keys: [ da1a339bd82d2c2e9180626a00dc043275deb3ababb27b5738abf6b9dcee8db6 ]
,pred: keys-all}}

Todo:

  • The gas costs are chosen arbitrarily. Do benchmarking to find the correct cost.
  • Gate the builtin behind a version flag.

Gas analysis:

The following code was used to benchmark decoding:

benchTokenMessages :: [Benchmark]
benchTokenMessages = 
  let
    !msg1 = mkTokenMessage 1
    name1 = "tokenmessage-" ++ show (T.length msg1)
    !msg1000 = mkTokenMessage 1000
    name1000 = "tokenmessage-" ++ show (T.length msg1000)
    !msg2000 = mkTokenMessage 2000
    name2000 = "tokenmessage-" ++ show (T.length msg2000)
  in
  [ bench name1 $ whnf hyperlaneDecodeTokenMessage msg1
  , bench name1000 $ whnf hyperlaneDecodeTokenMessage msg1000
  , bench name2000 $ whnf hyperlaneDecodeTokenMessage msg2000
  ]

The results are: 2 microseconds for a 256-byte message, 11 microseconds for an 10880-byte message, 21 microseconds for a 21548-byte message.

400 MilliGas = 1 microsecond by policy. 10k bytes = 10 microseconds by benchmarks. 100 bytes = 0.1 microseconds = 40 MilliGas. We round up to 50 MilliGas as a safety buffer.

PR checklist:

  • Test coverage for the proposed changes
  • PR description contains example output from repl interaction or a snippet from unit test output
  • Documentation has been updated if new natives or FV properties have been added. To generate new documentation, issue cabal run tests. If they pass locally, docs are generated.
  • Any changes that could be relevant to users have been recorded in the changelog
  • In case of changes to the Pact trace output (pact -t), make sure pact-lsp is in sync.

Additionally, please justify why you should or should not do the following:

  • Confirm replay/back compat
  • Benchmark regressions
  • (For Kadena engineers) Run integration-tests against a Chainweb built with this version of Pact

src/Pact/Native.hs Outdated Show resolved Hide resolved
src/Pact/Native.hs Outdated Show resolved Hide resolved
src/Pact/Native.hs Show resolved Hide resolved
src/Pact/Native.hs Outdated Show resolved Hide resolved
pact.cabal Outdated Show resolved Hide resolved
src/Pact/Native.hs Outdated Show resolved Hide resolved
src/Pact/Native.hs Outdated Show resolved Hide resolved
Copy link
Contributor

@edmundnoble edmundnoble left a comment

Choose a reason for hiding this comment

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

My approval is waiting on @ak3n's

pact.cabal Outdated
@@ -207,6 +207,7 @@ library
, base >= 4.18.0.0
, base16-bytestring >=0.1.1.6
, base64-bytestring >=1.0.0.1
, binary
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a lower bound


-- Parse the size of the following amount field.
amountSize <- fromIntegral @Word256 @Int <$> getWord256be
unless (amountSize == 96)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this 96? Does @ak3n know?

Copy link

Choose a reason for hiding this comment

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

That's not an amountSize but the offset of the recipient which is a string.

recipient is a first field of the TokenMessageERC20 structure and since it was encoded with abi.encode there is an offset:

0000000000000000000000000000000000000000000000000000000000000060 # offset of the recipient string = 96, because first three lines are 32 bytes each
0000000000000000000000000000000000000000000000008ac7230489e80000 # amount = 10000000000000000000
0000000000000000000000000000000000000000000000000000000000000000 # chainId = 0
000000000000000000000000000000000000000000000000000000000000002a # recipientSize = 42
3078373143373635364543376162383862303938646566423735314237343031 # "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"
4235663664383937364600000000000000000000000000000000000000000000

Let's leave this comment in the code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ak3n Thanks for the comment example! The only part I don't understand is the example recipient "0x71C7656EC7ab88b098defB751B7401B5f6d8976F". Our recipients are now JSON-encoded guards, e.g. { "pred": "keys-any", "keys": [...] }. Should we replace the hex string with that?

@@ -276,7 +276,7 @@ pact410Natives :: [Text]
pact410Natives = ["poseidon-hash-hack-a-chain"]

pact411Natives :: [Text]
pact411Natives = ["enforce-verifier", "hyperlane-message-id"]
pact411Natives = ["enforce-verifier", "hyperlane-message-id", "hyperlane-decode-token-message"]
Copy link

Choose a reason for hiding this comment

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

Either we need to specify the version hyperlane-v3-decode-token-message or pass an object with {"version": 3, ...} field.

@edmundnoble, should we also specify the type of the token? hyperlane-decode-token-message-erc20.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that's necessary, because tokenmessage is not actually part of the Hyperlane spec, so it's not versioned with it. We can make future versions, if they're needed, use -v2, -v3, etc.

src/Pact/Native.hs Outdated Show resolved Hide resolved

-- Parse the size of the following amount field.
amountSize <- fromIntegral @Word256 @Int <$> getWord256be
unless (amountSize == 96)
Copy link

Choose a reason for hiding this comment

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

That's not an amountSize but the offset of the recipient which is a string.

recipient is a first field of the TokenMessageERC20 structure and since it was encoded with abi.encode there is an offset:

0000000000000000000000000000000000000000000000000000000000000060 # offset of the recipient string = 96, because first three lines are 32 bytes each
0000000000000000000000000000000000000000000000008ac7230489e80000 # amount = 10000000000000000000
0000000000000000000000000000000000000000000000000000000000000000 # chainId = 0
000000000000000000000000000000000000000000000000000000000000002a # recipientSize = 42
3078373143373635364543376162383862303938646566423735314237343031 # "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"
4235663664383937364600000000000000000000000000000000000000000000

Let's leave this comment in the code

src/Pact/Native.hs Show resolved Hide resolved
where
getWord256be = get @Word256

-- TODO: We check the size. Is this ok?
Copy link

Choose a reason for hiding this comment

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

This is good 👍

But my comment was about the take and fromIntegral functions since size :: Word256 and we convert it to Int.

src/Pact/Native.hs Outdated Show resolved Hide resolved
@jmcardon jmcardon merged commit d5279f4 into master Feb 22, 2024
8 checks passed
@imalsogreg imalsogreg deleted the greg/hyperlane-decode-message branch February 22, 2024 21:38
jmcardon pushed a commit that referenced this pull request Feb 23, 2024
* Add hyperlane-decode-token-message with gas

* include chainid in TokenMessage encoding

* add principal check for recipient

* TokenMessage recipient parsed as guard

* add new builtin to pact411natives and suppress a debugging error message

* exhaust the TokenMessage when parsing

* use wide-word instead of doubleword

* remove stale TODOs

* cleanup more comments

* Set the empirically measured cas cost for TokenMessage decoding

* fix example in native definition

* fix examples

* rename builtin to hyperlane-decode-token-message

* use base64-unpadded encoding/decoding for tokenmessage

* fix gas costing and add gas tests

* lower bound for binary dependency

* cleanup tokenmessage decoder comments and variable names

* remove padding from encoded message in tokenmessage decoding example

* gasmodelspec remove buildin from list of gas-untested

* update example encoding
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants