From 3751854fe11ce69b01fae77310b7d696b0bc2234 Mon Sep 17 00:00:00 2001 From: chessai Date: Tue, 19 Mar 2024 18:26:32 -0500 Subject: [PATCH] add keccak256 native --- docs/en/pact-functions.md | 28 +++++++++-- golden/gas-model/golden | 7 +++ pact.cabal | 6 ++- src/Crypto/Hash/Keccak256Native.hs | 54 +++++++++++++++++++++ src/Pact/Gas/Table.hs | 11 +++++ src/Pact/GasModel/GasTests.hs | 12 +++++ src/Pact/Interpreter.hs | 10 +++- src/Pact/Native.hs | 59 ++++++++++++++++++++--- src/Pact/Types/Gas.hs | 3 ++ src/Pact/Types/Runtime.hs | 2 + tests/Keccak256Spec.hs | 77 ++++++++++++++++++++++++++++++ tests/PactTests.hs | 2 + tests/pact/keccak256.repl | 32 +++++++++++++ 13 files changed, 290 insertions(+), 13 deletions(-) create mode 100644 src/Crypto/Hash/Keccak256Native.hs create mode 100644 tests/Keccak256Spec.hs create mode 100644 tests/pact/keccak256.repl diff --git a/docs/en/pact-functions.md b/docs/en/pact-functions.md index 963e6540d..7697d28f2 100644 --- a/docs/en/pact-functions.md +++ b/docs/en/pact-functions.md @@ -461,7 +461,7 @@ Return ID if called during current pact execution, failing if not. Obtain current pact build version. ```lisp pact> (pact-version) -"4.10" +"4.11" ``` Top level only: this function will fail if used in module code. @@ -1796,7 +1796,25 @@ pact> (scalar-mult 'g1 {'x: 1, 'y: 2} 2) {"x": 1368015179489954701390400359078579693043519447331113978918064868415326638035,"y": 9918110051302171585080402603319702774565515993150576347155970296011118125764} ``` -## Poseidon Hash {#Poseidon Hash} +## Hashes {#Hashes} + +### hash-keccak256 {#hash-keccak256} + +*bytes* `[string]` *→* `string` + + +Compute the hash of a list of unpadded base64url-encoded inputs. The hash is computed incrementally over all of the decoded inputs. +```lisp +pact> (hash-keccak256 []) +"xdJGAYb3IzySfn2y3McDwOUAtlPKgic7e_rYBF2FpHA" +pact> (hash-keccak256 [""]) +"xdJGAYb3IzySfn2y3McDwOUAtlPKgic7e_rYBF2FpHA" +pact> (hash-keccak256 ["T73FllCNJKKgAQ4UCYC4CfucbVXsdRJYkd2YXTdmW9gPm-tqUCB1iKvzzu6Md82KWtSKngqgdO04hzg2JJbS-yyHVDuzNJ6mSZfOPntCTqktEi9X27CFWoAwWEN_4Ir7DItecXm5BEu_TYGnFjsxOeMIiLU2sPlX7_macWL0ylqnVqSpgt-tvzHvJVCDxLXGwbmaEH19Ov_9uJFHwsxMmiZD9Hjl4tOTrqN7THy0tel9rc8WtrUKrg87VJ7OR3Rtts5vZ91EBs1OdVldUQPRP536eTcpJNMo-N0fy-taji6L9Mdt4I4_xGqgIfmJxJMpx6ysWmiFVte8vLKl1L5p0yhOnEDsSDjuhZISDOIKC2NeytqoT9VpBQn1T3fjWkF8WEZIvJg5uXTge_qwA46QKV0LE5AlMKgw0cK91T8fnJ-u1Dyk7tCo3XYbx-292iiih8YM1Cr1-cdY5cclAjHAmlglY2ia_GXit5p6K2ggBmd1LpEBdG8DGE4jmeTtiDXLjprpDilq8iCuI0JZ_gvQvMYPekpf8_cMXtTenIxRmhDpYvZzyCxek1F4aoo7_VcAMYV71Mh_T8ox7U1Q4U8hB9oCy1BYcAt06iQai0HXhGFljxsrkL_YSkwsnWVDhhqzxWRRdX3PubpgMzSI290C1gG0Gq4xfKdHTrbm3Q"]) +"DqM-LjT1ckQGQCRMfx9fBGl86XE5vacqZVjYZjwCs4g" +pact> (hash-keccak256 ["T73FllCNJKKgAQ4UCYC4CfucbVXsdRJYkd2YXTdmW9g", "D5vralAgdYir887ujHfNilrUip4KoHTtOIc4NiSW0vs", "LIdUO7M0nqZJl84-e0JOqS0SL1fbsIVagDBYQ3_givs", "DItecXm5BEu_TYGnFjsxOeMIiLU2sPlX7_macWL0ylo", "p1akqYLfrb8x7yVQg8S1xsG5mhB9fTr__biRR8LMTJo", "JkP0eOXi05Ouo3tMfLS16X2tzxa2tQquDztUns5HdG0", "ts5vZ91EBs1OdVldUQPRP536eTcpJNMo-N0fy-taji4", "i_THbeCOP8RqoCH5icSTKcesrFpohVbXvLyypdS-adM", "KE6cQOxIOO6FkhIM4goLY17K2qhP1WkFCfVPd-NaQXw", "WEZIvJg5uXTge_qwA46QKV0LE5AlMKgw0cK91T8fnJ8", "rtQ8pO7QqN12G8ftvdoooofGDNQq9fnHWOXHJQIxwJo", "WCVjaJr8ZeK3mnoraCAGZ3UukQF0bwMYTiOZ5O2INcs", "jprpDilq8iCuI0JZ_gvQvMYPekpf8_cMXtTenIxRmhA", "6WL2c8gsXpNReGqKO_1XADGFe9TIf0_KMe1NUOFPIQc", "2gLLUFhwC3TqJBqLQdeEYWWPGyuQv9hKTCydZUOGGrM", "xWRRdX3PubpgMzSI290C1gG0Gq4xfKdHTrbm3Q"]) +"DqM-LjT1ckQGQCRMfx9fBGl86XE5vacqZVjYZjwCs4g" +``` + ### poseidon-hash-hack-a-chain {#poseidon-hash-hack-a-chain} @@ -1822,9 +1840,9 @@ pact> (poseidon-hash-hack-a-chain 1 2 3 4 5 6 7 8) *x* `string` *→* `object:*` -Decode a base-64 encoded Hyperlane Token Message into an object `{recipient:GUARD, amount:DECIMAL, chainId:STRING}`. +Decode a base-64-unpadded encoded Hyperlane Token Message into an object `{recipient:GUARD, amount:DECIMAL, chainId:STRING}`. ```lisp -pact> (hyperlane-decode-token-message "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGF7InByZWQiOiAia2V5cy1hbGwiLCAia2V5cyI6WyJkYTFhMzM5YmQ4MmQyYzJlOTE4MDYyNmEwMGRjMDQzMjc1ZGViM2FiYWJiMjdiNTczOGFiZjZiOWRjZWU4ZGI2Il19AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") +pact> (hyperlane-decode-token-message "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGF7InByZWQiOiAia2V5cy1hbGwiLCAia2V5cyI6WyJkYTFhMzM5YmQ4MmQyYzJlOTE4MDYyNmEwMGRjMDQzMjc1ZGViM2FiYWJiMjdiNTczOGFiZjZiOWRjZWU4ZGI2Il19AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") {"amount": 0.000000000000000123,"chainId": "4","recipient": KeySet {keys: [da1a339bd82d2c2e9180626a00dc043275deb3ababb27b5738abf6b9dcee8db6],pred: keys-all}} ``` @@ -1983,7 +2001,7 @@ Retreive any accumulated events and optionally clear event state. Object returne *→* `[string]` -Queries, or with arguments, sets execution config flags. Valid flags: ["AllowReadInLocal","DisableHistoryInTransactionalMode","DisableInlineMemCheck","DisableModuleInstall","DisableNewTrans","DisablePact40","DisablePact410","DisablePact411","DisablePact42","DisablePact43","DisablePact431","DisablePact44","DisablePact45","DisablePact46","DisablePact47","DisablePact48","DisablePact49","DisablePactEvents","DisableRuntimeReturnTypeChecking","EnforceKeyFormats","OldReadOnlyBehavior","PreserveModuleIfacesBug","PreserveModuleNameBug","PreserveNsModuleInstallBug","PreserveShowDefs"] +Queries, or with arguments, sets execution config flags. Valid flags: ["AllowReadInLocal","DisableHistoryInTransactionalMode","DisableInlineMemCheck","DisableModuleInstall","DisableNewTrans","DisablePact40","DisablePact410","DisablePact411","DisablePact412","DisablePact42","DisablePact43","DisablePact431","DisablePact44","DisablePact45","DisablePact46","DisablePact47","DisablePact48","DisablePact49","DisablePactEvents","DisableRuntimeReturnTypeChecking","EnforceKeyFormats","OldReadOnlyBehavior","PreserveModuleIfacesBug","PreserveModuleNameBug","PreserveNsModuleInstallBug","PreserveShowDefs"] ```lisp pact> (env-exec-config ['DisableHistoryInTransactionalMode]) (env-exec-config) ["DisableHistoryInTransactionalMode"] diff --git a/golden/gas-model/golden b/golden/gas-model/golden index 3f2ca1ce8..7c41279ca 100644 --- a/golden/gas-model/golden +++ b/golden/gas-model/golden @@ -126,6 +126,13 @@ - 5 - - (hash smallOjectMap) - 5 +- - |- + (hash-keccak256 []) + (hash-keccak256 [""]) + (hash-keccak256 ["IP9FQ2ml0FuBp489sFgZ_qmwjCOE91ywq2qhFd1pDaMTGHShyo9witFRnqlSweJJy1QNGWOSx56HdVQk_ufIkICMViciNZ7qUuihL7u5ad15YdK6UgN0k3VaX6BPDVChqibJtEFIwNO5TRxKWaMayhWui9RKy3gz2OkcS4b6MTWkIzh7gVG0Ez7SP21xh7UOwiBK2QGtdNOW5EJ04OyvquF7O5CF4iJgs1ylOxXMUqu6dYr2eY-9BOzuztZI869P2z3tdVeppc-3OCYSqKjz9FlH0aKc4pByko7Bk8ol1RBxvV4ZhOz0AvMG6nYvDyUoL1KW2Zdli-P5g2lv-m0JXGNptNr3nppdMTYikSj462PBK56fp4r_ej6eGaYgIkk80Tbe-7W7e6G5OPNn_S9j61ynbAsP8hueNsPwcjDPPDB05dpYcECnaXXX459ElKzlSG_L84CrdVjE_ollYzW4Lk24ZZUJ6rRqGWExJuWUBCcy3UxBH0GqjN6sccD7QKlObaVYwF53thgoBvJtmv3z2gDGlBkiLIGGpu-tYAtBDmzi8qeX5J3B8TUxmAH6bzlrBvl14qGQoCPkdLYY5w"]) + (hash-keccak256 ["IP9FQ2ml0FuBp489sFgZ_qmwjCOE91ywq2qhFd1pDaM", "Exh0ocqPcIrRUZ6pUsHiSctUDRljkseeh3VUJP7nyJA", "gIxWJyI1nupS6KEvu7lp3Xlh0rpSA3STdVpfoE8NUKE", "qibJtEFIwNO5TRxKWaMayhWui9RKy3gz2OkcS4b6MTU", "pCM4e4FRtBM-0j9tcYe1DsIgStkBrXTTluRCdODsr6o", "4Xs7kIXiImCzXKU7FcxSq7p1ivZ5j70E7O7O1kjzr08", "2z3tdVeppc-3OCYSqKjz9FlH0aKc4pByko7Bk8ol1RA", "cb1eGYTs9ALzBup2Lw8lKC9SltmXZYvj-YNpb_ptCVw", "Y2m02veeml0xNiKRKPjrY8Ernp-niv96Pp4ZpiAiSTw", "0Tbe-7W7e6G5OPNn_S9j61ynbAsP8hueNsPwcjDPPDA", "dOXaWHBAp2l11-OfRJSs5Uhvy_OAq3VYxP6JZWM1uC4", "TbhllQnqtGoZYTEm5ZQEJzLdTEEfQaqM3qxxwPtAqU4", "baVYwF53thgoBvJtmv3z2gDGlBkiLIGGpu-tYAtBDmw", "4vKnl-SdwfE1MZgB-m85awb5deKhkKAj5HS2GOc"]) + (hash-keccak256 ["T73FllCNJKKgAQ4UCYC4CfucbVXsdRJYkd2YXTdmW9gPm-tqUCB1iKvzzu6Md82KWtSKngqgdO04hzg2JJbS-yyHVDuzNJ6mSZfOPntCTqktEi9X27CFWoAwWEN_4Ir7DItecXm5BEu_TYGnFjsxOeMIiLU2sPlX7_macWL0ylqnVqSpgt-tvzHvJVCDxLXGwbmaEH19Ov_9uJFHwsxMmiZD9Hjl4tOTrqN7THy0tel9rc8WtrUKrg87VJ7OR3Rtts5vZ91EBs1OdVldUQPRP536eTcpJNMo-N0fy-taji6L9Mdt4I4_xGqgIfmJxJMpx6ysWmiFVte8vLKl1L5p0yhOnEDsSDjuhZISDOIKC2NeytqoT9VpBQn1T3fjWkF8WEZIvJg5uXTge_qwA46QKV0LE5AlMKgw0cK91T8fnJ-u1Dyk7tCo3XYbx-292iiih8YM1Cr1-cdY5cclAjHAmlglY2ia_GXit5p6K2ggBmd1LpEBdG8DGE4jmeTtiDXLjprpDilq8iCuI0JZ_gvQvMYPekpf8_cMXtTenIxRmhDpYvZzyCxek1F4aoo7_VcAMYV71Mh_T8ox7U1Q4U8hB9oCy1BYcAt06iQai0HXhGFljxsrkL_YSkwsnWVDhhqzxWRRdX3PubpgMzSI290C1gG0Gq4xfKdHTrbm3Q"]) + - 44 - - (* longNumber longNumber) - 3 - - (* medNumber medNumber) diff --git a/pact.cabal b/pact.cabal index 1e57732e3..841c44308 100644 --- a/pact.cabal +++ b/pact.cabal @@ -96,6 +96,7 @@ library cbits/musl/sqrt_data.c exposed-modules: Crypto.Hash.Blake2Native + Crypto.Hash.Keccak256Native Crypto.Hash.HyperlaneMessageId Crypto.Hash.PoseidonNative Pact.Analyze.Remote.Types @@ -222,11 +223,12 @@ library , direct-sqlite >=2.3.27 , directory >=1.2.6.2 , errors >=2.3 + , ethereum >= 0.1 , exceptions >=0.8.3 , filepath >=1.4.1.0 , groups , hashable >=1.4 - , ethereum >= 0.1 + , hashes >= 0.2 , lens >=4.14 , megaparsec >=9 , memory @@ -421,6 +423,7 @@ test-suite hspec , attoparsec , base , base16-bytestring + , base64-bytestring , bound , bytestring , containers @@ -471,6 +474,7 @@ test-suite hspec GoldenSpec HistoryServiceSpec HyperlaneSpec + Keccak256Spec PactContinuationSpec PersistSpec PoseidonSpec diff --git a/src/Crypto/Hash/Keccak256Native.hs b/src/Crypto/Hash/Keccak256Native.hs new file mode 100644 index 000000000..4ea05b2f0 --- /dev/null +++ b/src/Crypto/Hash/Keccak256Native.hs @@ -0,0 +1,54 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Implementation of the `keccak256` pact native. +-- +-- `keccak256` takes as input a Pact object representing a +-- 'HyperlaneMessage', and returns a base16-encoded hash of the abi-encoding +-- of the input. +module Crypto.Hash.Keccak256Native (Keccak256Error(..), keccak256) where + +import Control.Exception (Exception(..), SomeException(..), try) +import Control.Monad (forM_) +import Control.Monad.Catch (throwM) +import Data.ByteString.Short qualified as BSS +import Data.Hash.Class.Mutable (initialize, finalize, updateByteString) +import Data.Hash.Internal.OpenSSL (OpenSslException(..)) +import Data.Hash.Keccak (Keccak256(..)) +import Data.Text (Text) +import Data.Text.Encoding qualified as Text +import Data.Vector (Vector) +import Pact.Types.Util (encodeBase64UrlUnpadded, decodeBase64UrlUnpadded) +import System.IO.Unsafe (unsafePerformIO) + +data Keccak256Error + = Keccak256OpenSslException String + | Keccak256Base64Exception String + deriving stock (Eq, Show) + deriving anyclass (Exception) + +keccak256 :: Vector Text -> Either Keccak256Error Text +keccak256 strings = unsafePerformIO $ do + e <- try @SomeException @_ $ do + ctx <- initialize @Keccak256 + forM_ strings $ \string -> do + case decodeBase64UrlUnpadded (Text.encodeUtf8 string) of + Left b64Err -> do + throwM (Keccak256Base64Exception b64Err) + Right bytes -> do + updateByteString @Keccak256 ctx bytes + Keccak256 hash <- finalize ctx + pure (BSS.fromShort hash) + case e of + Left err + | Just (OpenSslException msg) <- fromException err -> pure (Left (Keccak256OpenSslException msg)) + | Just (exc :: Keccak256Error) <- fromException err -> pure (Left exc) + | otherwise -> error "keccak256 failed" + Right hash -> pure (Right (Text.decodeUtf8 (encodeBase64UrlUnpadded hash))) +{-# noinline keccak256 #-} diff --git a/src/Pact/Gas/Table.hs b/src/Pact/Gas/Table.hs index 8c1451d5f..facc91534 100644 --- a/src/Pact/Gas/Table.hs +++ b/src/Pact/Gas/Table.hs @@ -57,6 +57,8 @@ data GasCostConfig = GasCostConfig , _gasCostConfig_poseidonHashHackAChainLinearGasFactor :: Gas , _gasCostConfig_hyperlaneMessageIdGasPerRecipientOneHundredBytes :: MilliGas , _gasCostConfig_hyperlaneDecodeTokenMessageGasPerOneHundredBytes :: MilliGas + , _gasCostConfig_keccak256GasPerOneHundredBytes :: MilliGas + , _gasCostConfig_keccak256GasPerChunk :: MilliGas } defaultGasConfig :: GasCostConfig @@ -85,6 +87,8 @@ defaultGasConfig = GasCostConfig , _gasCostConfig_poseidonHashHackAChainQuadraticGasFactor = 38 , _gasCostConfig_hyperlaneMessageIdGasPerRecipientOneHundredBytes = MilliGas 47 , _gasCostConfig_hyperlaneDecodeTokenMessageGasPerOneHundredBytes = MilliGas 50 + , _gasCostConfig_keccak256GasPerOneHundredBytes = MilliGas 146 + , _gasCostConfig_keccak256GasPerChunk = MilliGas 2_120 } defaultGasTable :: Map Text Gas @@ -242,6 +246,7 @@ defaultGasTable = ,("poseidon-hash-hack-a-chain", 124) ,("hyperlane-message-id", 2) ,("hyperlane-decode-token-message", 2) + ,("hash-keccak256",1) ] {-# NOINLINE defaultGasTable #-} @@ -345,6 +350,12 @@ tableGasModel gasConfig = GHyperlaneDecodeTokenMessage len -> let MilliGas costPerOneHundredBytes = _gasCostConfig_hyperlaneDecodeTokenMessageGasPerOneHundredBytes gasConfig in MilliGas (costPerOneHundredBytes * div (fromIntegral len) 100) + GKeccak256 numBytesInChunk -> + let MilliGas costPerOneHundredBytes = _gasCostConfig_keccak256GasPerOneHundredBytes gasConfig + MilliGas costPerChunk = _gasCostConfig_keccak256GasPerChunk gasConfig + -- we need to use ceiling here, otherwise someone could cheat by + -- having as many bytes as they want, but in chunks of 99 bytes. + in MilliGas (costPerChunk + costPerOneHundredBytes * ceiling (fromIntegral @_ @Double numBytesInChunk / 100.0)) in GasModel { gasModelName = "table" diff --git a/src/Pact/GasModel/GasTests.hs b/src/Pact/GasModel/GasTests.hs index 6f804b7fd..43216e5d9 100644 --- a/src/Pact/GasModel/GasTests.hs +++ b/src/Pact/GasModel/GasTests.hs @@ -228,6 +228,7 @@ allTests = HM.fromList -- SPI/Hyperlane , ("hyperlane-message-id", hyperlaneMessageIdTests) , ("enforce-verifier", enforceVerifierTests) + , ("hash-keccak256", keccak256Tests) -- Non-native concepts to benchmark , ("use", useTests) @@ -2044,3 +2045,14 @@ hyperlaneMessageIdTests = defGasUnitTest $ PactExpression hyperlaneMessageIdExpr (hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"},"version": 1}) (hyperlane-message-id {"destinationDomain": 1,"nonce": 325,"originDomain": 626,"recipient": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F","sender": "0x6b622d746f6b656e2d726f75746572","tokenMessage": {"amount": 10000000000000000000.0,"recipient": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"},"version": 1}) |] + +keccak256Tests :: NativeDefName -> GasUnitTests +keccak256Tests = defGasUnitTest $ PactExpression keccak256ExprText Nothing + where + keccak256ExprText = [text| + (hash-keccak256 []) + (hash-keccak256 [""]) + (hash-keccak256 ["IP9FQ2ml0FuBp489sFgZ_qmwjCOE91ywq2qhFd1pDaMTGHShyo9witFRnqlSweJJy1QNGWOSx56HdVQk_ufIkICMViciNZ7qUuihL7u5ad15YdK6UgN0k3VaX6BPDVChqibJtEFIwNO5TRxKWaMayhWui9RKy3gz2OkcS4b6MTWkIzh7gVG0Ez7SP21xh7UOwiBK2QGtdNOW5EJ04OyvquF7O5CF4iJgs1ylOxXMUqu6dYr2eY-9BOzuztZI869P2z3tdVeppc-3OCYSqKjz9FlH0aKc4pByko7Bk8ol1RBxvV4ZhOz0AvMG6nYvDyUoL1KW2Zdli-P5g2lv-m0JXGNptNr3nppdMTYikSj462PBK56fp4r_ej6eGaYgIkk80Tbe-7W7e6G5OPNn_S9j61ynbAsP8hueNsPwcjDPPDB05dpYcECnaXXX459ElKzlSG_L84CrdVjE_ollYzW4Lk24ZZUJ6rRqGWExJuWUBCcy3UxBH0GqjN6sccD7QKlObaVYwF53thgoBvJtmv3z2gDGlBkiLIGGpu-tYAtBDmzi8qeX5J3B8TUxmAH6bzlrBvl14qGQoCPkdLYY5w"]) + (hash-keccak256 ["IP9FQ2ml0FuBp489sFgZ_qmwjCOE91ywq2qhFd1pDaM", "Exh0ocqPcIrRUZ6pUsHiSctUDRljkseeh3VUJP7nyJA", "gIxWJyI1nupS6KEvu7lp3Xlh0rpSA3STdVpfoE8NUKE", "qibJtEFIwNO5TRxKWaMayhWui9RKy3gz2OkcS4b6MTU", "pCM4e4FRtBM-0j9tcYe1DsIgStkBrXTTluRCdODsr6o", "4Xs7kIXiImCzXKU7FcxSq7p1ivZ5j70E7O7O1kjzr08", "2z3tdVeppc-3OCYSqKjz9FlH0aKc4pByko7Bk8ol1RA", "cb1eGYTs9ALzBup2Lw8lKC9SltmXZYvj-YNpb_ptCVw", "Y2m02veeml0xNiKRKPjrY8Ernp-niv96Pp4ZpiAiSTw", "0Tbe-7W7e6G5OPNn_S9j61ynbAsP8hueNsPwcjDPPDA", "dOXaWHBAp2l11-OfRJSs5Uhvy_OAq3VYxP6JZWM1uC4", "TbhllQnqtGoZYTEm5ZQEJzLdTEEfQaqM3qxxwPtAqU4", "baVYwF53thgoBvJtmv3z2gDGlBkiLIGGpu-tYAtBDmw", "4vKnl-SdwfE1MZgB-m85awb5deKhkKAj5HS2GOc"]) + (hash-keccak256 ["T73FllCNJKKgAQ4UCYC4CfucbVXsdRJYkd2YXTdmW9gPm-tqUCB1iKvzzu6Md82KWtSKngqgdO04hzg2JJbS-yyHVDuzNJ6mSZfOPntCTqktEi9X27CFWoAwWEN_4Ir7DItecXm5BEu_TYGnFjsxOeMIiLU2sPlX7_macWL0ylqnVqSpgt-tvzHvJVCDxLXGwbmaEH19Ov_9uJFHwsxMmiZD9Hjl4tOTrqN7THy0tel9rc8WtrUKrg87VJ7OR3Rtts5vZ91EBs1OdVldUQPRP536eTcpJNMo-N0fy-taji6L9Mdt4I4_xGqgIfmJxJMpx6ysWmiFVte8vLKl1L5p0yhOnEDsSDjuhZISDOIKC2NeytqoT9VpBQn1T3fjWkF8WEZIvJg5uXTge_qwA46QKV0LE5AlMKgw0cK91T8fnJ-u1Dyk7tCo3XYbx-292iiih8YM1Cr1-cdY5cclAjHAmlglY2ia_GXit5p6K2ggBmd1LpEBdG8DGE4jmeTtiDXLjprpDilq8iCuI0JZ_gvQvMYPekpf8_cMXtTenIxRmhDpYvZzyCxek1F4aoo7_VcAMYV71Mh_T8ox7U1Q4U8hB9oCy1BYcAt06iQai0HXhGFljxsrkL_YSkwsnWVDhhqzxWRRdX3PubpgMzSI290C1gG0Gq4xfKdHTrbm3Q"]) + |] diff --git a/src/Pact/Interpreter.hs b/src/Pact/Interpreter.hs index c7c035b23..97eb772e6 100644 --- a/src/Pact/Interpreter.hs +++ b/src/Pact/Interpreter.hs @@ -254,6 +254,9 @@ disablePact410Natives = disablePactNatives pact410Natives FlagDisablePact410 disablePact411Natives :: ExecutionConfig -> Endo RefStore disablePact411Natives = disablePactNatives pact411Natives FlagDisablePact411 +disablePact412Natives :: ExecutionConfig -> Endo RefStore +disablePact412Natives = disablePactNatives pact412Natives FlagDisablePact412 + pact40Natives :: [Text] pact40Natives = ["enumerate" , "distinct" , "emit-event" , "concat" , "str-to-list"] @@ -278,6 +281,9 @@ pact410Natives = ["poseidon-hash-hack-a-chain"] pact411Natives :: [Text] pact411Natives = ["enforce-verifier", "hyperlane-message-id", "hyperlane-decode-token-message"] +pact412Natives :: [Text] +pact412Natives = ["hash-keccak256"] + initRefStore :: RefStore initRefStore = RefStore nativeDefs @@ -292,7 +298,9 @@ versionedNativesRefStore ec = versionNatives initRefStore , disablePact46Natives ec , disablePact47Natives ec , disablePact410Natives ec - , disablePact411Natives ec ] + , disablePact411Natives ec + , disablePact412Natives ec + ] mkSQLiteEnv :: Logger -> Bool -> PSL.SQLiteConfig -> Loggers -> IO (PactDbEnv (DbEnv PSL.SQLite)) mkSQLiteEnv initLog deleteOldFile c loggers = do diff --git a/src/Pact/Native.hs b/src/Pact/Native.hs index 5eb69ed4d..ed306ad80 100644 --- a/src/Pact/Native.hs +++ b/src/Pact/Native.hs @@ -117,6 +117,7 @@ import Pact.Types.Purity import Pact.Types.Runtime import Pact.Types.Version import Pact.Types.Namespace +import Crypto.Hash.Keccak256Native (Keccak256Error(..), keccak256) import Crypto.Hash.PoseidonNative (poseidon) import Crypto.Hash.HyperlaneMessageId (hyperlaneMessageId) @@ -136,11 +137,10 @@ natives = , decryptDefs , guardDefs , zkDefs - , poseidonHackAChainDefs + , hashDefs , hyperlaneDefs ] - -- | Production native modules as a dispatch map. nativeDefs :: HM.HashMap Text Ref nativeDefs = mconcat $ map moduleToMap natives @@ -148,7 +148,6 @@ nativeDefs = mconcat $ map moduleToMap natives moduleToMap :: NativeModule -> HM.HashMap Text Ref moduleToMap = HM.fromList . map (asString *** Direct) . snd - lengthDef :: NativeDef lengthDef = defRNative "length" length' (funType tTyInteger [("x",listA)]) ["(length [1 2 3])", "(length \"abcdefgh\")", "(length { \"a\": 1, \"b\": 2 })"] @@ -1564,8 +1563,11 @@ base64DecodeWithShimmedErrors i txt = do Nothing -> evalError i "Could not parse error message" -poseidonHackAChainDefs :: NativeModule -poseidonHackAChainDefs = ("Poseidon Hash", [ poseidonHackAChainDef ]) +hashDefs :: NativeModule +hashDefs = ("Hashes",) + [ poseidonHackAChainDef + , keccak256Def + ] poseidonHackAChainDef :: NativeDef poseidonHackAChainDef = defGasRNative @@ -1587,6 +1589,51 @@ poseidonHackAChainDef = defGasRNative return $ toTerm $ poseidon intArgs | otherwise = argsError i as +keccak256Def :: NativeDef +keccak256Def = defGasRNative + "hash-keccak256" + keccak256' + (funType tTyString [("bytes", TyList tTyString)]) + [ + "(hash-keccak256 [])" + , "(hash-keccak256 [\"\"])" + , "(hash-keccak256 [\"T73FllCNJKKgAQ4UCYC4CfucbVXsdRJYkd2YXTdmW9gPm-tqUCB1iKvzzu6Md82KWtSKngqgdO04hzg2JJbS-yyHVDuzNJ6mSZfOPntCTqktEi9X27CFWoAwWEN_4Ir7DItecXm5BEu_TYGnFjsxOeMIiLU2sPlX7_macWL0ylqnVqSpgt-tvzHvJVCDxLXGwbmaEH19Ov_9uJFHwsxMmiZD9Hjl4tOTrqN7THy0tel9rc8WtrUKrg87VJ7OR3Rtts5vZ91EBs1OdVldUQPRP536eTcpJNMo-N0fy-taji6L9Mdt4I4_xGqgIfmJxJMpx6ysWmiFVte8vLKl1L5p0yhOnEDsSDjuhZISDOIKC2NeytqoT9VpBQn1T3fjWkF8WEZIvJg5uXTge_qwA46QKV0LE5AlMKgw0cK91T8fnJ-u1Dyk7tCo3XYbx-292iiih8YM1Cr1-cdY5cclAjHAmlglY2ia_GXit5p6K2ggBmd1LpEBdG8DGE4jmeTtiDXLjprpDilq8iCuI0JZ_gvQvMYPekpf8_cMXtTenIxRmhDpYvZzyCxek1F4aoo7_VcAMYV71Mh_T8ox7U1Q4U8hB9oCy1BYcAt06iQai0HXhGFljxsrkL_YSkwsnWVDhhqzxWRRdX3PubpgMzSI290C1gG0Gq4xfKdHTrbm3Q\"])" + , "(hash-keccak256 [\"T73FllCNJKKgAQ4UCYC4CfucbVXsdRJYkd2YXTdmW9g\", \"D5vralAgdYir887ujHfNilrUip4KoHTtOIc4NiSW0vs\", \"LIdUO7M0nqZJl84-e0JOqS0SL1fbsIVagDBYQ3_givs\", \"DItecXm5BEu_TYGnFjsxOeMIiLU2sPlX7_macWL0ylo\", \"p1akqYLfrb8x7yVQg8S1xsG5mhB9fTr__biRR8LMTJo\", \"JkP0eOXi05Ouo3tMfLS16X2tzxa2tQquDztUns5HdG0\", \"ts5vZ91EBs1OdVldUQPRP536eTcpJNMo-N0fy-taji4\", \"i_THbeCOP8RqoCH5icSTKcesrFpohVbXvLyypdS-adM\", \"KE6cQOxIOO6FkhIM4goLY17K2qhP1WkFCfVPd-NaQXw\", \"WEZIvJg5uXTge_qwA46QKV0LE5AlMKgw0cK91T8fnJ8\", \"rtQ8pO7QqN12G8ftvdoooofGDNQq9fnHWOXHJQIxwJo\", \"WCVjaJr8ZeK3mnoraCAGZ3UukQF0bwMYTiOZ5O2INcs\", \"jprpDilq8iCuI0JZ_gvQvMYPekpf8_cMXtTenIxRmhA\", \"6WL2c8gsXpNReGqKO_1XADGFe9TIf0_KMe1NUOFPIQc\", \"2gLLUFhwC3TqJBqLQdeEYWWPGyuQv9hKTCydZUOGGrM\", \"xWRRdX3PubpgMzSI290C1gG0Gq4xfKdHTrbm3Q\"])" + ] + "Compute the hash of a list of unpadded base64url-encoded inputs. The hash is computed incrementally over all of the decoded inputs." + where + keccak256' :: RNativeFun e + keccak256' i = \case + args@[TList ls _ _] -> do + texts <- do + let mTexts = forM ls $ \case + TLiteral (LString s) _ -> Just s + _ -> Nothing + case mTexts of + Nothing -> argsError i args + Just x -> pure x + + forM_ texts $ \chunk -> do + -- At first I thought we should charge based on decoded bytes. + -- Both base64 and base64url have predictable byte-lengths from + -- both the input and output. However, charging on the encoded + -- length makes more sense, for two reasons: + -- + -- 1. We do not know if the decoding will fail here, and that + -- decoding is best left to the impl, not the nativedef + -- 2. Using the encoded bytes will potentially overcharge a bit, + -- but that's better than undercharging + let numBytesInChunk = BS.length (T.encodeUtf8 chunk) + computeGas' i (GKeccak256 numBytesInChunk) (pure ()) + + output <- case keccak256 texts of + Left (Keccak256Base64Exception msg) -> evalError' i ("Base64URL decode failed: " <> prettyString msg) + Left (Keccak256OpenSslException msg) -> evalError' i ("OpenSSL error when hashing: " <> prettyString msg) + Right output -> pure output + pure (toTerm output) + + args -> argsError i args + hyperlaneDefs :: NativeModule hyperlaneDefs = ("Hyperlane",) [ hyperlaneMessageIdDef @@ -1653,7 +1700,7 @@ hyperlaneDecodeTokenMessageDef = -- 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" + Left _ -> evalError' i "Decoding error: binary decoding failed" Right (_,_,(amount, chain, recipient)) -> case PGuard <$> J.eitherDecode (BS.fromStrict $ T.encodeUtf8 recipient) of Left _ -> evalError' i $ "Could not parse recipient into a guard" diff --git a/src/Pact/Types/Gas.hs b/src/Pact/Types/Gas.hs index 55c0643a8..bd8332f95 100644 --- a/src/Pact/Types/Gas.hs +++ b/src/Pact/Types/Gas.hs @@ -189,6 +189,8 @@ data GasArgs | GHyperlaneDecodeTokenMessage !Int -- ^ Cost of hyperlane-decode-token-message on this size (in bytes) of the -- hyperlane TokenMessage base64-encoded string. + | GKeccak256 !Int + -- ^ Cost of keccak256 per number of bytes in a chunk. data IntOpThreshold = Pact43IntThreshold @@ -259,6 +261,7 @@ instance Pretty GasArgs where GPoseidonHashHackAChain len -> "GPoseidonHashHackAChain:" <> pretty len GHyperlaneMessageId len -> "GHyperlaneMessageId:" <> pretty len GHyperlaneDecodeTokenMessage len -> "GHyperlaneDecodeTokenMessage:" <> pretty len + GKeccak256 numBytesInChunk -> "GKeccak256:" <> pretty numBytesInChunk newtype GasLimit = GasLimit ParsedInteger deriving (Eq,Ord,Generic) diff --git a/src/Pact/Types/Runtime.hs b/src/Pact/Types/Runtime.hs index dbfdca106..b826cfba9 100644 --- a/src/Pact/Types/Runtime.hs +++ b/src/Pact/Types/Runtime.hs @@ -205,6 +205,8 @@ data ExecutionFlag | FlagDisablePact410 -- | Disable Pact 4.11 Features | FlagDisablePact411 + -- | Disable Pact 4.12 Features + | FlagDisablePact412 deriving (Eq,Ord,Show,Enum,Bounded) -- | Flag string representation diff --git a/tests/Keccak256Spec.hs b/tests/Keccak256Spec.hs new file mode 100644 index 000000000..bf2baf949 --- /dev/null +++ b/tests/Keccak256Spec.hs @@ -0,0 +1,77 @@ +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} + +module Keccak256Spec (spec) where + +import Control.Monad (forM_) +import Crypto.Hash.Keccak256Native (keccak256) +import Data.ByteString.Base16 qualified as Base16 +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Text.Encoding qualified as Text +import Data.Vector qualified as V +import Pact.Types.Util (decodeBase64UrlUnpadded, encodeBase64UrlUnpadded) +import Test.Hspec (Spec, describe, it, shouldBe) + +data Reference = Reference + { input :: Text + , output :: Text + } + +-- | Test vectors taken from https://keccak.team/archives.html. +-- "Known-answer and Monte Carlo test results, as of round 3 of the SHA-3 competition": +-- https://keccak.team/obsolete/KeccakKAT-3.zip +references :: [Reference] +references = + [ Reference + { input = "" + , output = "C5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470" + } + , Reference + { input = "20FF454369A5D05B81A78F3DB05819FEA9B08C2384F75CB0AB6AA115DD690DA3131874A1CA8F708AD1519EA952C1E249CB540D196392C79E87755424FEE7C890808C562722359EEA52E8A12FBBB969DD7961D2BA52037493755A5FA04F0D50A1AA26C9B44148C0D3B94D1C4A59A31ACA15AE8BD44ACB7833D8E91C4B86FA3135A423387B8151B4133ED23F6D7187B50EC2204AD901AD74D396E44274E0ECAFAAE17B3B9085E22260B35CA53B15CC52ABBA758AF6798FBD04ECEECED648F3AF4FDB3DED7557A9A5CFB7382612A8A8F3F45947D1A29CE29072928EC193CA25D51071BD5E1984ECF402F306EA762F0F25282F5296D997658BE3F983696FFA6D095C6369B4DAF79E9A5D3136229128F8EB63C12B9E9FA78AFF7A3E9E19A62022493CD136DEFBB5BB7BA1B938F367FD2F63EB5CA76C0B0FF21B9E36C3F07230CF3C3074E5DA587040A76975D7E39F4494ACE5486FCBF380AB7558C4FE89656335B82E4DB8659509EAB46A19613126E594042732DD4C411F41AA8CDEAC71C0FB40A94E6DA558C05E77B6182806F26D9AFDF3DA00C69419222C8186A6EFAD600B410E6CE2F2A797E49DC1F135319801FA6F396B06F975E2A190A023E474B618E7" + , output = "0EC8D9D20DDF0A7B0251E941A7261B557507FF6287B504362A8F1734C5A91012" + } + , Reference + { input = "4FBDC596508D24A2A0010E140980B809FB9C6D55EC75125891DD985D37665BD80F9BEB6A50207588ABF3CEEE8C77CD8A5AD48A9E0AA074ED388738362496D2FB2C87543BB3349EA64997CE3E7B424EA92D122F57DBB0855A803058437FE08AFB0C8B5E7179B9044BBF4D81A7163B3139E30888B536B0F957EFF99A7162F4CA5AA756A4A982DFADBF31EF255083C4B5C6C1B99A107D7D3AFFFDB89147C2CC4C9A2643F478E5E2D393AEA37B4C7CB4B5E97DADCF16B6B50AAE0F3B549ECE47746DB6CE6F67DD4406CD4E75595D5103D13F9DFA79372924D328F8DD1FCBEB5A8E2E8BF4C76DE08E3FC46AA021F989C49329C7ACAC5A688556D7BCBCB2A5D4BE69D3284E9C40EC4838EE8592120CE20A0B635ECADAA84FD5690509F54F77E35A417C584648BC9839B974E07BFAB0038E90295D0B13902530A830D1C2BDD53F1F9C9FAED43CA4EED0A8DD761BC7EDBDDA28A287C60CD42AF5F9C758E5C7250231C09A582563689AFC65E2B79A7A2B68200667752E9101746F03184E2399E4ED8835CB8E9AE90E296AF220AE234259FE0BD0BCC60F7A4A5FF3F70C5ED4DE9C8C519A10E962F673C82C5E9351786A8A3BFD570031857BD4C87F4FCA31ED4D50E14F2107DA02CB5058700B74EA241A8B41D78461658F1B2B90BFD84A4C2C9D6543861AB3C56451757DCFB9BA60333488DBDD02D601B41AAE317CA7474EB6E6DD" + , output = "0EA33E2E34F572440640244C7F1F5F04697CE97139BDA72A6558D8663C02B388" + } + ] + +spec :: Spec +spec = describe "keccak256" $ do + it "produces correct results" $ do + forM_ references $ \ref -> do + hashInputAndCompareToOutput ref.input ref.output + hashChunksAreEqual ref.input + +hash :: Text -> Text +hash str = hashChunks [str] + +hashChunks :: [Text] -> Text +hashChunks chunks = unwrap go + where + go :: Either String Text + go = do + vals <- traverse (Base16.decode . Text.encodeUtf8) chunks + let texts = V.fromList $ map (Text.decodeUtf8 . encodeBase64UrlUnpadded) vals + case keccak256 texts of + Left err -> Left (show err) + Right out -> do + rawHash <- decodeBase64UrlUnpadded (Text.encodeUtf8 out) + pure (Text.decodeUtf8 (Base16.encode rawHash)) + +hashInputAndCompareToOutput :: Text -> Text -> IO () +hashInputAndCompareToOutput i o = do + Text.toLower (hash i) `shouldBe` Text.toLower o + +hashChunksAreEqual :: Text -> IO () +hashChunksAreEqual original = do + let chunkSize = 10 -- random number i made up + let chunks = Text.chunksOf chunkSize original + Text.toLower (hash original) `shouldBe` Text.toLower (hashChunks chunks) + +unwrap :: Either String a -> a +unwrap e = case e of + Left err -> error err + Right a -> a diff --git a/tests/PactTests.hs b/tests/PactTests.hs index f8cedd77c..56b26e8e1 100644 --- a/tests/PactTests.hs +++ b/tests/PactTests.hs @@ -25,6 +25,7 @@ import qualified GasModelSpec import qualified GoldenSpec import qualified HistoryServiceSpec import qualified HyperlaneSpec +import qualified Keccak256Spec import qualified PactContinuationSpec import qualified PersistSpec import qualified RemoteVerifySpec @@ -62,6 +63,7 @@ main = hspec $ parallel $ do describe "GoldenSpec" GoldenSpec.spec describe "HistoryServiceSpec" HistoryServiceSpec.spec describe "HyperlaneSpec" HyperlaneSpec.spec + describe "Keccak256Spec" Keccak256Spec.spec describe "PactContinuationSpec" PactContinuationSpec.spec describe "PersistSpec" PersistSpec.spec describe "RemoteVerifySpec" RemoteVerifySpec.spec diff --git a/tests/pact/keccak256.repl b/tests/pact/keccak256.repl new file mode 100644 index 000000000..893bdd36f --- /dev/null +++ b/tests/pact/keccak256.repl @@ -0,0 +1,32 @@ +(expect "computes the correct hash" + "xdJGAYb3IzySfn2y3McDwOUAtlPKgic7e_rYBF2FpHA" + (hash-keccak256 []) +) + +(expect "computes the correct hash" + "xdJGAYb3IzySfn2y3McDwOUAtlPKgic7e_rYBF2FpHA" + (hash-keccak256 [""]) +) + +(expect "computes the correct hash" + "DsjZ0g3fCnsCUelBpyYbVXUH_2KHtQQ2Ko8XNMWpEBI" + (hash-keccak256 ["IP9FQ2ml0FuBp489sFgZ_qmwjCOE91ywq2qhFd1pDaMTGHShyo9witFRnqlSweJJy1QNGWOSx56HdVQk_ufIkICMViciNZ7qUuihL7u5ad15YdK6UgN0k3VaX6BPDVChqibJtEFIwNO5TRxKWaMayhWui9RKy3gz2OkcS4b6MTWkIzh7gVG0Ez7SP21xh7UOwiBK2QGtdNOW5EJ04OyvquF7O5CF4iJgs1ylOxXMUqu6dYr2eY-9BOzuztZI869P2z3tdVeppc-3OCYSqKjz9FlH0aKc4pByko7Bk8ol1RBxvV4ZhOz0AvMG6nYvDyUoL1KW2Zdli-P5g2lv-m0JXGNptNr3nppdMTYikSj462PBK56fp4r_ej6eGaYgIkk80Tbe-7W7e6G5OPNn_S9j61ynbAsP8hueNsPwcjDPPDB05dpYcECnaXXX459ElKzlSG_L84CrdVjE_ollYzW4Lk24ZZUJ6rRqGWExJuWUBCcy3UxBH0GqjN6sccD7QKlObaVYwF53thgoBvJtmv3z2gDGlBkiLIGGpu-tYAtBDmzi8qeX5J3B8TUxmAH6bzlrBvl14qGQoCPkdLYY5w"]) +) + +(expect "computes the correct hash over chunked input" + "DsjZ0g3fCnsCUelBpyYbVXUH_2KHtQQ2Ko8XNMWpEBI" + (hash-keccak256 ["IP9FQ2ml0FuBp489sFgZ_qmwjCOE91ywq2qhFd1pDaM", "Exh0ocqPcIrRUZ6pUsHiSctUDRljkseeh3VUJP7nyJA", "gIxWJyI1nupS6KEvu7lp3Xlh0rpSA3STdVpfoE8NUKE", "qibJtEFIwNO5TRxKWaMayhWui9RKy3gz2OkcS4b6MTU", "pCM4e4FRtBM-0j9tcYe1DsIgStkBrXTTluRCdODsr6o", "4Xs7kIXiImCzXKU7FcxSq7p1ivZ5j70E7O7O1kjzr08", "2z3tdVeppc-3OCYSqKjz9FlH0aKc4pByko7Bk8ol1RA", "cb1eGYTs9ALzBup2Lw8lKC9SltmXZYvj-YNpb_ptCVw", "Y2m02veeml0xNiKRKPjrY8Ernp-niv96Pp4ZpiAiSTw", "0Tbe-7W7e6G5OPNn_S9j61ynbAsP8hueNsPwcjDPPDA", "dOXaWHBAp2l11-OfRJSs5Uhvy_OAq3VYxP6JZWM1uC4", "TbhllQnqtGoZYTEm5ZQEJzLdTEEfQaqM3qxxwPtAqU4", "baVYwF53thgoBvJtmv3z2gDGlBkiLIGGpu-tYAtBDmw", "4vKnl-SdwfE1MZgB-m85awb5deKhkKAj5HS2GOc"]) +) + +(expect "computes the correct hash" + "DqM-LjT1ckQGQCRMfx9fBGl86XE5vacqZVjYZjwCs4g" + (hash-keccak256 ["T73FllCNJKKgAQ4UCYC4CfucbVXsdRJYkd2YXTdmW9gPm-tqUCB1iKvzzu6Md82KWtSKngqgdO04hzg2JJbS-yyHVDuzNJ6mSZfOPntCTqktEi9X27CFWoAwWEN_4Ir7DItecXm5BEu_TYGnFjsxOeMIiLU2sPlX7_macWL0ylqnVqSpgt-tvzHvJVCDxLXGwbmaEH19Ov_9uJFHwsxMmiZD9Hjl4tOTrqN7THy0tel9rc8WtrUKrg87VJ7OR3Rtts5vZ91EBs1OdVldUQPRP536eTcpJNMo-N0fy-taji6L9Mdt4I4_xGqgIfmJxJMpx6ysWmiFVte8vLKl1L5p0yhOnEDsSDjuhZISDOIKC2NeytqoT9VpBQn1T3fjWkF8WEZIvJg5uXTge_qwA46QKV0LE5AlMKgw0cK91T8fnJ-u1Dyk7tCo3XYbx-292iiih8YM1Cr1-cdY5cclAjHAmlglY2ia_GXit5p6K2ggBmd1LpEBdG8DGE4jmeTtiDXLjprpDilq8iCuI0JZ_gvQvMYPekpf8_cMXtTenIxRmhDpYvZzyCxek1F4aoo7_VcAMYV71Mh_T8ox7U1Q4U8hB9oCy1BYcAt06iQai0HXhGFljxsrkL_YSkwsnWVDhhqzxWRRdX3PubpgMzSI290C1gG0Gq4xfKdHTrbm3Q"]) +) + +(expect-failure "fails on non-base64 inputs" + (hash-keccak256 ["alibaba"]) +) + +(expect-failure "fails if any inputs are not base64-encoded" + (hash-keccak256 ["Zm9v", "fooey"]) +)