-
Notifications
You must be signed in to change notification settings - Fork 116
Add a builtin for decoding TokenMessage for hyperlane SPI #1344
Changes from 13 commits
9c3f015
0774aca
53494f7
3d2a171
b96ebc6
bd9fb7b
5d6c156
e2b65c1
3631cb8
9601df1
68a1f51
6e7086a
89d10ca
f899c60
2e2703d
091c68f
3a61688
16f278c
4e7f70f
bd6d3cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either we need to specify the version @edmundnoble, should we also specify the type of the token?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| initRefStore :: RefStore | ||
| initRefStore = RefStore nativeDefs | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| {-# LANGUAGE RecordWildCards #-} | ||
| {-# LANGUAGE ScopedTypeVariables #-} | ||
| {-# LANGUAGE TupleSections #-} | ||
| {-# LANGUAGE TypeApplications #-} | ||
| {-# LANGUAGE ViewPatterns #-} | ||
| {-# LANGUAGE MultiWayIf #-} | ||
| -- | | ||
|
|
@@ -55,6 +56,7 @@ module Pact.Native | |
| , describeNamespaceSchema | ||
| , dnUserGuard, dnAdminGuard, dnNamespaceName | ||
| , cdPrevBlockHash | ||
| , encodeTokenMessage | ||
| ) where | ||
|
|
||
| import Control.Arrow hiding (app, first) | ||
|
|
@@ -64,22 +66,30 @@ import Control.Monad | |
| import Control.Monad.IO.Class | ||
| import qualified Data.Attoparsec.Text as AP | ||
| import Data.Bifunctor (first) | ||
| import Data.Binary (get, put) | ||
| import Data.Binary.Get (Get, runGetOrFail, getByteString, isEmpty) | ||
| import Data.Binary.Put (Put, runPut, putByteString) | ||
imalsogreg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import Data.Bool (bool) | ||
| import qualified Data.ByteString as BS | ||
| import qualified Data.ByteString.Base64 as B64 | ||
| import qualified Data.ByteString.Base64.URL as B64URL | ||
| import qualified Data.Char as Char | ||
| import Data.Bits | ||
| import Data.Decimal (Decimal) | ||
| import Data.Default | ||
| import Data.Functor(($>)) | ||
| import Data.Foldable | ||
| import Data.List (isPrefixOf) | ||
| import qualified Data.HashMap.Strict as HM | ||
| import qualified Data.Map.Strict as M | ||
| import qualified Data.List as L (nubBy) | ||
| import Data.Ratio ((%)) | ||
| import qualified Data.Set as S | ||
| import Data.Text (Text, pack, unpack) | ||
| import qualified Data.Text as T | ||
| import qualified Data.Text as Text | ||
| import qualified Data.Text.Encoding as T | ||
| import Data.WideWord.Word256 | ||
| import Pact.Time | ||
| import qualified Data.Vector as V | ||
| import qualified Data.Vector.Algorithms.Intro as V | ||
|
|
@@ -111,6 +121,7 @@ import Crypto.Hash.PoseidonNative (poseidon) | |
| import Crypto.Hash.HyperlaneMessageId (hyperlaneMessageId) | ||
|
|
||
| import qualified Pact.JSON.Encode as J | ||
| import qualified Pact.JSON.Decode as J | ||
|
|
||
| -- | All production native modules. | ||
| natives :: [NativeModule] | ||
|
|
@@ -1579,6 +1590,7 @@ poseidonHackAChainDef = defGasRNative | |
| hyperlaneDefs :: NativeModule | ||
| hyperlaneDefs = ("Hyperlane",) | ||
| [ hyperlaneMessageIdDef | ||
| , hyperlaneDecodeTokenMessageDef | ||
| ] | ||
|
|
||
| hyperlaneMessageIdDef :: NativeDef | ||
|
|
@@ -1609,3 +1621,112 @@ hyperlaneMessageIdDef = defGasRNative | |
| case mRecipient of | ||
| Nothing -> error "couldn't decode token recipient" | ||
| Just t -> T.encodeUtf8 t | ||
|
|
||
| hyperlaneDecodeTokenMessageDef :: NativeDef | ||
| hyperlaneDecodeTokenMessageDef = | ||
| defGasRNative | ||
| "hyperlane-decode-token-message" | ||
| hyperlaneDecodeTokenMessageDef' | ||
| (funType tTyObjectAny [("x", tTyString)]) | ||
| ["(hyperlane-decode-token-message \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGF7InByZWQiOiAia2V5cy1hbGwiLCAia2V5cyI6WyJkYTFhMzM5YmQ4MmQyYzJlOTE4MDYyNmEwMGRjMDQzMjc1ZGViM2FiYWJiMjdiNTczOGFiZjZiOWRjZWU4ZGI2Il19AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\")"] | ||
| "Decode a base-64 encoded Hyperlane Token Message into an object `{recipient:GUARD, amount:DECIMAL, chainId:STRING}`." | ||
jmcardon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| where | ||
| hyperlaneDecodeTokenMessageDef' :: RNativeFun e | ||
| hyperlaneDecodeTokenMessageDef' i args = case args of | ||
|
|
||
| [TLitString msg] -> | ||
| -- We do not need to handle historical b64 error message shimming | ||
| -- or decoding from non-canonical strings in this base-64 decoder, | ||
| -- because this native is added in a Pact version that later than when | ||
| -- we moved to base64-bytestring >= 1.0, which behaves succeeds and | ||
| -- fails in exactly the cases we expect. | ||
| -- (The only change we make to its output is to strip error messages). | ||
| computeGas' i (GHyperlaneDecodeTokenMessage (T.length msg)) $ | ||
| case B64URL.decode (T.encodeUtf8 msg) of | ||
| Left _ -> evalError' i "Failed to base64-decode token message" | ||
| Right bytes -> do | ||
| case runGetOrFail (getTokenMessageERC20 <* eof) (BS.fromStrict bytes) of | ||
| -- In case of Binary decoding failure, emit a terse error message. | ||
| -- If the error message begins with TokenError, we know that we | ||
| -- created it, and it is going to be stable (non-forking). | ||
| -- If it does not start with TokenMessage, it may have come from | ||
| -- the Binary library, and we will suppress it to shield ourselves | ||
| -- from forking behavior if we update our Binary version. | ||
| Left (_,_,e) | "TokenMessage" `isPrefixOf` e -> evalError' i $ "Decoding error: " <> pretty e | ||
| Left _ -> evalError' i "Decoding error: binary decoding failed" | ||
imalsogreg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Right (_,_,(amount, chain, recipient)) -> | ||
| case PGuard <$> J.eitherDecode (BS.fromStrict $ T.encodeUtf8 recipient) of | ||
| Left _ -> evalError' i $ "Could not parse recipient into a guard" | ||
| Right g -> | ||
| pure $ toTObject TyAny def | ||
| [("recipient", fromPactValue g) | ||
| ,("amount", TLiteral (LDecimal $ wordToDecimal amount) def) | ||
| ,("chainId", toTerm chain) | ||
| ] | ||
| _ -> argsError i args | ||
|
|
||
| -- The TokenMessage contains a recipient (text) and an amount (word-256). | ||
| getTokenMessageERC20 :: Get (Word256, ChainId, Text) | ||
| getTokenMessageERC20 = do | ||
|
|
||
| -- Parse the size of the following amount field. | ||
| amountSize <- fromIntegral @Word256 @Int <$> getWord256be | ||
| unless (amountSize == 96) | ||
|
||
| (fail $ "TokenMessage amountSize expected 96, found " ++ show amountSize) | ||
| tmAmount <- getWord256be | ||
| tmChainId <- getWord256be | ||
|
|
||
| recipientSize <- getWord256be | ||
| tmRecipient <- T.decodeUtf8 <$> getRecipient recipientSize | ||
|
|
||
| return (tmAmount, ChainId { _chainId = T.pack (show (toInteger tmChainId))}, tmRecipient) | ||
| where | ||
| getWord256be = get @Word256 | ||
|
|
||
| -- TODO: We check the size. Is this ok? | ||
|
||
| -- | Reads a given number of bytes and the rest because binary data padded up to 32 bytes. | ||
| getRecipient :: Word256 -> Get BS.ByteString | ||
| getRecipient size = do | ||
| recipient <- BS.take (fromIntegral size) <$> getByteString (fromIntegral $ size + restSize size) | ||
| if BS.length recipient < fromIntegral size | ||
| then fail "TokenMessage recipient was smaller than expected" | ||
| else pure recipient | ||
|
|
||
|
|
||
| wordToDecimal :: Word256 -> Decimal | ||
| wordToDecimal w = | ||
| let ethInWei = 1000000000000000000 -- 1e18 | ||
| in fromRational (toInteger w % ethInWei) | ||
|
|
||
| eof :: Get () | ||
| eof = do | ||
| done <- isEmpty | ||
| unless done $ fail "pending bytes in input" | ||
|
|
||
| -- | Helper function for creating TokenMessages encoded in the ERC20 format | ||
| -- and base64url encoded. Used for generating test data. | ||
| encodeTokenMessage :: BS.ByteString -> Word256 -> Word256 -> Text | ||
| encodeTokenMessage recipient amount chain = T.decodeUtf8 $ B64URL.encode (BS.toStrict bytes) | ||
| where | ||
| bytes = runPut $ do | ||
| putWord256be (96 :: Word256) | ||
| putWord256be amount | ||
| putWord256be chain | ||
| putWord256be recipientSize | ||
| putByteString recipientBytes | ||
|
|
||
| (recipientBytes, recipientSize) = padRight recipient | ||
|
|
||
| putWord256be :: Word256 -> Put | ||
| putWord256be = put @Word256 | ||
|
|
||
| padRight :: BS.ByteString -> (BS.ByteString, Word256) | ||
| padRight s = | ||
| let | ||
| size = BS.length s | ||
| missingZeroes = restSize size | ||
| in (s <> BS.replicate missingZeroes 0, fromIntegral size) | ||
|
|
||
| -- | Returns the modular of 32 bytes. | ||
| restSize :: Integral a => a -> a | ||
| restSize size = (32 - size) `mod` 32 | ||
imalsogreg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| ;; Test hyperlane builtins. | ||
|
|
||
| (env-data | ||
| { "test-keys" : {"pred": "keys-all", "keys": ["da1a339bd82d2c2e9180626a00dc043275deb3ababb27b5738abf6b9dcee8db6"]} | ||
| }) | ||
|
|
||
| (expect "computes the correct message id" "0x97d98aa7fdb548f43c9be37aaea33fca79680247eb8396148f1df10e6e0adfb7" (hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"},"version": 1})) | ||
|
|
||
| ; Decoding a valid TokenMessage should succeed. | ||
| (expect "decodes the correct TokenMessage" | ||
| { "amount":0.000000000000000123, | ||
| "chainId": "4", | ||
| "recipient": (read-keyset 'test-keys) | ||
| } | ||
| (hyperlane-decode-token-message "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGF7InByZWQiOiAia2V5cy1hbGwiLCAia2V5cyI6WyJkYTFhMzM5YmQ4MmQyYzJlOTE4MDYyNmEwMGRjMDQzMjc1ZGViM2FiYWJiMjdiNTczOGFiZjZiOWRjZWU4ZGI2Il19AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") | ||
| ) | ||
|
|
||
| ; This TokenMessage was encoded with the recipient | ||
| ; "k:462e97a099987f55f6a2b52e7bfd52a36b4b5b470fed0816a3d9b26f9450ba69". | ||
| ; It should fail to decode because "k:462e97a099987f55f6a2b52e7bfd52a36b4b5b470fed0816a3d9b26f9450ba69" | ||
| ; is a principal, not a guard. (Recipient must be a guard encoded in json). | ||
| (expect-failure | ||
| "Decoding requires recipient to be a guard." | ||
| (hyperlane-decode-token-message "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEJrOjQ2MmU5N2EwOTk5ODdmNTVmNmEyYjUyZTdiZmQ1MmEzNmI0YjViNDcwZmVkMDgxNmEzZDliMjZmOTQ1MGJhNjkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") | ||
| ) |
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.
Needs a lower bound