Skip to content

Commit f9f3143

Browse files
authored
add hyperlane-message-id native (#1335)
* add hyperlane-message-id native * remove benchmark code * add unit test * convert milligas to gas in hyperlane-message-id defaultGasTable * factor out repetitive prisms * add hyperlane-message-id repl test * add example to hyperlane-message-id Pact Native * round hyperlane-message-id gas constant up instead of down * add module-level documentation to HyperlaneMessageId.hs * move ghc-option for no missed extra shared lib to cabal.project * move demon let into where * regenerate docs * move demon let to where * add a gas model golden test for enforce-verifier * add hyperlane-message-id behind DisableVerifiers flag
1 parent 2c655e1 commit f9f3143

File tree

14 files changed

+350
-11
lines changed

14 files changed

+350
-11
lines changed

cabal.project

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
packages: .
22

3+
package pact
4+
ghc-options: -Wno-missed-extra-shared-lib
5+
6+
source-repository-package
7+
type: git
8+
location: https://github.com/kadena-io/pact-json.git
9+
tag: 1d260bfaa48312b54851057885de4c43c420e35f
10+
--sha256: 0fzq4mzaszj5clvixx9mn1x6r4dcrnwvbl2znd0p5mmy5h2jr0hh
11+
312
-- temporary upper bounds
413
constraints: sbv <10
514

615
-- test upper bounds
716
constraints: hspec-golden <0.2,
817

9-
source-repository-package
10-
type: git
11-
tag: e43073d0b8d89d9b300980913b842f4be339846d
12-
location: https://github.com/kadena-io/pact-json
13-
--sha256: sha256-ZWbAId0JBaxDsYhwcYUyw04sjYstXyosSCenzOvUxsQ=
14-
1518
-- These packages are tightly bundled with GHC. The rules ensure that
1619
-- our builds use the version that ships with the GHC version that is
1720
-- used for the build.
@@ -38,6 +41,9 @@ allow-newer: servant:*
3841
-- Required by trifecta (e.g. to allow mtl >=2.3)
3942
allow-newer: trifecta:*
4043

41-
-- servant-0.20 does not yet support aeson-2.2
42-
--
43-
constraints: aeson <2.2
44+
source-repository-package
45+
type: git
46+
location: https://github.com/kadena-io/kadena-ethereum-bridge.git
47+
tag: ffbf20e9f0430b95448bd66c6b1b530864397fb3
48+
--sha256: sha256-xdawv/tdjh61MbJKcBqm9Fje36+gVljuZsAxOTX1gP0=
49+

docs/en/pact-functions.md

+13
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,19 @@ pact> (poseidon-hash-hack-a-chain 1 2 3 4 5 6 7 8)
18151815
18604317144381847857886385684060986177838410221561136253933256952257712543953
18161816
```
18171817

1818+
## Hyperlane {#Hyperlane}
1819+
1820+
### hyperlane-message-id {#hyperlane-message-id}
1821+
1822+
*x*&nbsp;`object:*` *&rarr;*&nbsp;`string`
1823+
1824+
1825+
Get the Message Id of a Hyperlane Message object.
1826+
```lisp
1827+
pact> (hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"},"version": 1})
1828+
"0x97d98aa7fdb548f43c9be37aaea33fca79680247eb8396148f1df10e6e0adfb7"
1829+
```
1830+
18181831
## REPL-only functions {#repl-lib}
18191832

18201833
The following functions are loaded automatically into the interactive REPL, or within script files with a `.repl` extension. They are not available for blockchain-based execution.

golden/gas-model/golden

+14
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,10 @@
597597
"8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"
598598
"77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
599599
- 29
600+
- - |-
601+
(hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"},"version": 1})
602+
(hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"},"version": 1})
603+
- 4
600604
- - (^ 2 longNumber)
601605
- 4
602606
- - (^ 2 medNumber)
@@ -999,6 +1003,16 @@
9991003
- 2
10001004
- - (>= (time "2016-07-22T12:00:00Z") (time "2018-07-22T12:00:00Z"))
10011005
- 6
1006+
- - |-
1007+
(module m GOV
1008+
(defcap GOV () true)
1009+
1010+
(defcap GOOD () (enforce-verifier 'HYPERLANE))
1011+
1012+
(defun good () (with-capability (GOOD) 1))
1013+
)
1014+
(good)
1015+
- 59
10021016
- - (take 1 longNumberList)
10031017
- 3
10041018
- - (take 1 medNumberList)

pact.cabal

+4
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ library
9696
cbits/musl/sqrt_data.c
9797
exposed-modules:
9898
Crypto.Hash.Blake2Native
99+
Crypto.Hash.HyperlaneMessageId
99100
Crypto.Hash.PoseidonNative
100101
Pact.Analyze.Remote.Types
101102
Pact.ApiReq
@@ -224,6 +225,7 @@ library
224225
, filepath >=1.4.1.0
225226
, groups
226227
, hashable >=1.4
228+
, ethereum >= 0.1
227229
, lens >=4.14
228230
, megaparsec >=9
229231
, memory
@@ -254,6 +256,7 @@ library
254256
, vector >=0.11.0.0
255257
, vector-algorithms >=0.7
256258
, vector-space >=0.10.4
259+
, wide-word >= 0.1
257260
, yaml
258261

259262
if flag(build-tool)
@@ -466,6 +469,7 @@ test-suite hspec
466469
GasModelSpec
467470
GoldenSpec
468471
HistoryServiceSpec
472+
HyperlaneSpec
469473
PactContinuationSpec
470474
PersistSpec
471475
PoseidonSpec

src/Crypto/Hash/HyperlaneMessageId.hs

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE ImportQualifiedPost #-}
3+
{-# LANGUAGE NumericUnderscores #-}
4+
{-# LANGUAGE OverloadedStrings #-}
5+
{-# LANGUAGE RankNTypes #-}
6+
{-# LANGUAGE RecordWildCards #-}
7+
{-# LANGUAGE TypeApplications #-}
8+
9+
-- | Implementation of the `hyperlane-message-id` pact native.
10+
--
11+
-- `hyperlane-message-id` takes as input a Pact object representing a
12+
-- 'HyperlaneMessage', and returns a base16-encoded hash of the abi-encoding
13+
-- of the input.
14+
module Crypto.Hash.HyperlaneMessageId (hyperlaneMessageId) where
15+
16+
import Control.Error.Util (hush)
17+
import Control.Lens ((^?), at, _Just, Prism', _1)
18+
import Data.ByteString (ByteString)
19+
import Data.ByteString qualified as BS
20+
import Data.ByteString.Base16 qualified as Base16
21+
import Data.ByteString.Builder (Builder)
22+
import Data.ByteString.Builder qualified as BB
23+
import Data.ByteString.Lazy qualified as BL
24+
import Data.ByteString.Short qualified as BSS
25+
import Data.Decimal (Decimal)
26+
import Data.Map (Map)
27+
import Data.Text (Text)
28+
import Data.Text qualified as Text
29+
import Data.Text.Encoding qualified as Text
30+
import Data.WideWord.Word256 (Word256(..))
31+
import Data.Word (Word8, Word32)
32+
import Ethereum.Misc (keccak256, _getKeccak256Hash, _getBytesN)
33+
import Pact.Types.Runtime (Object(..), ObjectMap(..), FieldKey, Name, Literal, _TLiteral, _TObject, _LDecimal, _LInteger, _LString)
34+
import Pact.Types.Term (Term)
35+
36+
----------------------------------------------
37+
-- Primitive --
38+
----------------------------------------------
39+
40+
hyperlaneMessageId :: Object Name -> Text
41+
hyperlaneMessageId o = case decodeHyperlaneMessageObject o of
42+
Nothing -> error "Couldn't decode HyperlaneMessage"
43+
Just hm -> getHyperlaneMessageId hm
44+
45+
----------------------------------------------
46+
-- Hyperlane Message Encoding --
47+
----------------------------------------------
48+
49+
data HyperlaneMessage = HyperlaneMessage
50+
{ hmVersion :: Word8 -- uint8
51+
, hmNonce :: Word32 -- uint32
52+
, hmOriginDomain :: Word32 -- uint32
53+
, hmSender :: ByteString -- 32x uint8
54+
, hmDestinationDomain :: Word32 -- uint32
55+
, hmRecipient :: ByteString -- 32x uint8
56+
, hmTokenMessage :: TokenMessageERC20 -- variable
57+
}
58+
59+
packHyperlaneMessage :: HyperlaneMessage -> Builder
60+
packHyperlaneMessage (HyperlaneMessage{..}) =
61+
BB.word8 hmVersion
62+
<> BB.word32BE hmNonce
63+
<> BB.word32BE hmOriginDomain
64+
<> BB.byteString (padLeft hmSender)
65+
<> BB.word32BE hmDestinationDomain
66+
<> BB.byteString (padLeft hmRecipient)
67+
<> packTokenMessageERC20 hmTokenMessage
68+
69+
data TokenMessageERC20 = TokenMessageERC20
70+
{ tmRecipient :: Text -- variable
71+
, tmAmount :: Word256 -- uint256
72+
, tmChainId :: Maybe Word256 -- uint256
73+
}
74+
75+
packTokenMessageERC20 :: TokenMessageERC20 -> Builder
76+
packTokenMessageERC20 t =
77+
word256BE 64
78+
<> word256BE (tmAmount t)
79+
80+
<> word256BE recipientSize
81+
<> BB.byteString recipient
82+
where
83+
(recipient, recipientSize) = padRight (Text.encodeUtf8 (tmRecipient t))
84+
85+
word256BE :: Word256 -> Builder
86+
word256BE (Word256 a b c d) =
87+
BB.word64BE a <> BB.word64BE b <> BB.word64BE c <> BB.word64BE d
88+
89+
-- | Pad with zeroes on the left to 32 bytes
90+
--
91+
-- > padLeft "hello world"
92+
-- "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NULhello world"
93+
padLeft :: ByteString -> ByteString
94+
padLeft s = BS.replicate (32 - BS.length s) 0 <> s
95+
96+
-- | Pad with zeroes on the right, such that the resulting size is a multiple of 32.
97+
--
98+
-- > padRight "hello world"
99+
-- ("hello world\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL",11)
100+
padRight :: ByteString -> (ByteString, Word256)
101+
padRight s =
102+
let
103+
size = BS.length s
104+
missingZeroes = restSize size
105+
in (s <> BS.replicate missingZeroes 0, fromIntegral size)
106+
107+
-- | Returns the modular of 32 bytes.
108+
restSize :: Integral a => a -> a
109+
restSize size = (32 - size) `mod` 32
110+
111+
----------------------------------------------
112+
-- Hyperlane Message Hashing --
113+
----------------------------------------------
114+
115+
getHyperlaneMessageId :: HyperlaneMessage -> Text
116+
getHyperlaneMessageId =
117+
encodeHex
118+
. keccak256Hash
119+
. BL.toStrict
120+
. BB.toLazyByteString
121+
. packHyperlaneMessage
122+
123+
keccak256Hash :: ByteString -> ByteString
124+
keccak256Hash = BSS.fromShort . _getBytesN . _getKeccak256Hash . keccak256
125+
126+
encodeHex :: ByteString -> Text
127+
encodeHex b = "0x" <> Text.decodeUtf8 (Base16.encode b)
128+
129+
decodeHex :: Text -> Maybe ByteString
130+
decodeHex s = do
131+
h <- Text.stripPrefix "0x" s
132+
hush (Base16.decode (Text.encodeUtf8 h))
133+
134+
----------------------------------------------
135+
-- Hyperlane Pact Object Decoding --
136+
----------------------------------------------
137+
138+
decodeHyperlaneMessageObject :: Object Name -> Maybe HyperlaneMessage
139+
decodeHyperlaneMessageObject o = do
140+
let om = _objectMap (_oObject o)
141+
142+
hmVersion <- fromIntegral @Integer @Word8 <$> grabField om "version" _LInteger
143+
hmNonce <- fromIntegral @Integer @Word32 <$> grabField om "nonce" _LInteger
144+
hmOriginDomain <- fromIntegral @Integer @Word32 <$> grabField om "originDomain" _LInteger
145+
hmSender <- Text.encodeUtf8 <$> grabField om "sender" _LString
146+
hmDestinationDomain <- fromIntegral @Integer @Word32 <$> grabField om "destinationDomain" _LInteger
147+
hmRecipient <- decodeHex =<< grabField om "recipient" _LString
148+
149+
let tokenObject = om ^? at "tokenMessage" . _Just . _TObject . _1
150+
hmTokenMessage <- case decodeTokenMessageERC20 =<< tokenObject of
151+
Just t -> pure t
152+
_ -> error "Couldn't encode TokenMessageERC20"
153+
154+
pure HyperlaneMessage{..}
155+
156+
decodeTokenMessageERC20 :: Object Name -> Maybe TokenMessageERC20
157+
decodeTokenMessageERC20 o = do
158+
let om = _objectMap (_oObject o)
159+
tmRecipient <- grabField om "recipient" _LString
160+
tmAmount <- decimalToWord <$> grabField om "amount" _LDecimal
161+
let tmChainId = Nothing
162+
pure $ TokenMessageERC20{..}
163+
164+
decimalToWord :: Decimal -> Word256
165+
decimalToWord d =
166+
let ethInWei = 1_000_000_000_000_000_000 -- 1e18
167+
in round $ d * ethInWei
168+
169+
grabField :: Map FieldKey (Term Name) -> FieldKey -> Prism' Literal a -> Maybe a
170+
grabField m key p = m ^? at key . _Just . _TLiteral . _1 . p

src/Pact/Gas/Table.hs

+6
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ data GasCostConfig = GasCostConfig
5555
, _gasCostConfig_formatBytesPerGas :: Gas
5656
, _gasCostConfig_poseidonHashHackAChainQuadraticGasFactor :: Gas
5757
, _gasCostConfig_poseidonHashHackAChainLinearGasFactor :: Gas
58+
, _gasCostConfig_hyperlaneMessageIdGasPerRecipientOneHundredBytes :: MilliGas
5859
}
5960

6061
defaultGasConfig :: GasCostConfig
@@ -81,6 +82,7 @@ defaultGasConfig = GasCostConfig
8182
, _gasCostConfig_formatBytesPerGas = 10
8283
, _gasCostConfig_poseidonHashHackAChainLinearGasFactor = 50
8384
, _gasCostConfig_poseidonHashHackAChainQuadraticGasFactor = 38
85+
, _gasCostConfig_hyperlaneMessageIdGasPerRecipientOneHundredBytes = MilliGas 47
8486
}
8587

8688
defaultGasTable :: Map Text Gas
@@ -236,6 +238,7 @@ defaultGasTable =
236238
,("pairing-check", 1)
237239

238240
,("poseidon-hash-hack-a-chain", 124)
241+
,("hyperlane-message-id", 2)
239242
]
240243

241244
{-# NOINLINE defaultGasTable #-}
@@ -333,6 +336,9 @@ tableGasModel gasConfig =
333336
gasToMilliGas $
334337
_gasCostConfig_poseidonHashHackAChainQuadraticGasFactor gasConfig * fromIntegral (len * len) +
335338
_gasCostConfig_poseidonHashHackAChainLinearGasFactor gasConfig * fromIntegral len
339+
GHyperlaneMessageId len ->
340+
let MilliGas costPerOneHundredBytes = _gasCostConfig_hyperlaneMessageIdGasPerRecipientOneHundredBytes gasConfig
341+
in MilliGas (costPerOneHundredBytes * div (fromIntegral len) 100)
336342

337343
in GasModel
338344
{ gasModelName = "table"

src/Pact/GasModel/GasTests.hs

+35
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import qualified Data.Aeson as A
2323
import qualified Data.Foldable as F
2424
import qualified Data.HashMap.Strict as HM
2525
import qualified Data.Map as M
26+
import qualified Data.Set as S
2627
import qualified Data.Text as T
2728
import qualified Data.Text.Encoding as T
2829

@@ -35,6 +36,7 @@ import Pact.Types.Capability
3536
import Pact.Types.Lang
3637
import Pact.Types.PactValue (PactValue(..))
3738
import Pact.Types.Runtime
39+
import Pact.Types.Verifier (VerifierName(..))
3840
import Pact.JSON.Legacy.Value
3941

4042

@@ -223,6 +225,10 @@ allTests = HM.fromList
223225
, ("pairing-check", pairingCheckTests)
224226
, ("poseidon-hash-hack-a-chain", poseidonHashTests)
225227

228+
-- SPI/Hyperlane
229+
, ("hyperlane-message-id", hyperlaneMessageIdTests)
230+
, ("enforce-verifier", enforceVerifierTests)
231+
226232
-- Non-native concepts to benchmark
227233
, ("use", useTests)
228234
, ("module", moduleTests)
@@ -2009,3 +2015,32 @@ poseidonHashTests = defGasUnitTest $ PactExpression poseidonHashExprText Nothing
20092015
(poseidon-hash-hack-a-chain 1 2)
20102016
(poseidon-hash-hack-a-chain 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888)
20112017
|]
2018+
2019+
enforceVerifierTests :: NativeDefName -> GasUnitTests
2020+
enforceVerifierTests = createGasUnitTests signEnvWithKeySet signEnvWithKeySet [PactExpression enforceVerifierExprText Nothing]
2021+
where
2022+
verifMap :: M.Map VerifierName (S.Set SigCapability)
2023+
verifMap = M.fromList
2024+
[ (VerifierName "HYPERLANE", S.fromList [SigCapability (QualifiedName "m" "GOOD" def) []])
2025+
]
2026+
2027+
signEnvWithKeySet = setEnv (set eeMsgVerifiers verifMap)
2028+
2029+
enforceVerifierExprText = [text|
2030+
(module m GOV
2031+
(defcap GOV () true)
2032+
2033+
(defcap GOOD () (enforce-verifier 'HYPERLANE))
2034+
2035+
(defun good () (with-capability (GOOD) 1))
2036+
)
2037+
(good)
2038+
|]
2039+
2040+
hyperlaneMessageIdTests :: NativeDefName -> GasUnitTests
2041+
hyperlaneMessageIdTests = defGasUnitTest $ PactExpression hyperlaneMessageIdExprText Nothing
2042+
where
2043+
hyperlaneMessageIdExprText = [text|
2044+
(hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"},"version": 1})
2045+
(hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"},"version": 1})
2046+
|]

src/Pact/Interpreter.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ pact410Natives :: [Text]
276276
pact410Natives = ["poseidon-hash-hack-a-chain"]
277277

278278
verifierNatives :: [Text]
279-
verifierNatives = ["enforce-verifier"]
279+
verifierNatives = ["enforce-verifier", "hyperlane-message-id"]
280280

281281
initRefStore :: RefStore
282282
initRefStore = RefStore nativeDefs

0 commit comments

Comments
 (0)