mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-11-06 03:58:20 +00:00
* name registration primitives for sponsored path * 2 rounds of goal iter * 7 rounds of /goal
406 lines
21 KiB
Haskell
406 lines
21 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
|
|
module CoreTests.EthCryptoTests (ethCryptoTests) where
|
|
|
|
import Control.Concurrent.STM (TVar, atomically)
|
|
import Control.Monad (forM_)
|
|
import Crypto.Number.Serialize (i2osp)
|
|
import Crypto.Random (ChaChaDRG)
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.ByteArray as BA
|
|
import qualified Data.ByteArray.Encoding as BAE
|
|
import Data.ByteString (ByteString)
|
|
import qualified Data.ByteString as B
|
|
import qualified Data.ByteString.Char8 as BC
|
|
import Data.Char (toLower)
|
|
import Data.Either (isLeft, isRight)
|
|
import Data.FileEmbed (embedFile)
|
|
import Data.List (elemIndex, foldl', nub)
|
|
import qualified Data.Map.Strict as M
|
|
import Data.Maybe (fromJust, isNothing, listToMaybe)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
|
import Data.Word (Word32)
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import qualified Simplex.Messaging.Crypto.BIP32 as B32
|
|
import qualified Simplex.Messaging.Crypto.BIP39 as B39
|
|
import Simplex.Messaging.Crypto.BIP39.English (englishWordList)
|
|
import Simplex.Messaging.Crypto.BIP44
|
|
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
|
import Simplex.Messaging.Encoding.String (strDecode, strEncode)
|
|
import Simplex.Messaging.Eth.Address
|
|
import Simplex.Messaging.Eth.EIP712
|
|
import Simplex.Messaging.Eth.RLP
|
|
import Simplex.Messaging.Eth.Transaction
|
|
import Simplex.Messaging.Util ((<$$>))
|
|
import Test.Hspec hiding (fit, it)
|
|
import Util
|
|
|
|
ethCryptoTests :: Spec
|
|
ethCryptoTests = do
|
|
g <- runIO C.newRandom
|
|
describe "Keccak-256" keccakTests
|
|
describe "secp256k1" $ secp256k1Tests g
|
|
describe "BIP-39" $ bip39Tests g
|
|
describe "BIP-32" $ bip32Tests g
|
|
describe "BIP-44 derivation" $ derivationTests g
|
|
describe "EIP-55 addresses" eip55Tests
|
|
describe "RLP" rlpTests
|
|
describe "recoverable signing" $ signingTests g
|
|
describe "EIP-1559 transactions" $ transactionTests g
|
|
describe "EIP-712 typed data" eip712Tests
|
|
|
|
-- helpers
|
|
|
|
hx :: BA.ByteArray a => ByteString -> a
|
|
hx s = either (const $ error $ "bad hex literal: " <> BC.unpack s) id $ BAE.convertFromBase BAE.Base16 s
|
|
|
|
toHex :: BA.ByteArrayAccess a => a -> ByteString
|
|
toHex = BAE.convertToBase BAE.Base16
|
|
|
|
right :: Either String a -> a
|
|
right = either (error . ("unexpected Left: " <>)) id
|
|
|
|
xkHex :: B32.ExtendedKey -> (ByteString, ByteString)
|
|
xkHex xk = (toHex . S.unPrivateKey $ B32.xkKey xk, toHex $ B32.xkChainCode xk)
|
|
|
|
-- | Key and chain code of a base58check @xprv@: 4 version, 1 depth, 4 fingerprint, 4 child, 32 chain code, 0x00 and 32 key bytes, 4 checksum.
|
|
xprvHex :: ByteString -> (ByteString, ByteString)
|
|
xprvHex s
|
|
| B.length payload /= 78 || B.index payload 45 /= 0 || checksum /= B.take 4 (C.sha256Hash $ C.sha256Hash payload) = error $ "bad xprv: " <> BC.unpack s
|
|
| otherwise = (toHex $ B.drop 46 payload, toHex $ B.take 32 $ B.drop 13 payload)
|
|
where
|
|
(payload, checksum) = B.splitAt 78 $ base58Decode s
|
|
|
|
base58Decode :: ByteString -> ByteString
|
|
base58Decode s = B.replicate (B.length zeros) 0 <> i2osp n
|
|
where
|
|
(zeros, digits) = BC.span (== '1') s
|
|
n = foldl' (\acc c -> acc * 58 + fromIntegral (digit c)) (0 :: Integer) (BC.unpack digits)
|
|
digit c = fromJust $ elemIndex c "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
|
|
-- fixtures
|
|
|
|
-- https://github.com/trezor/python-mnemonic/blob/master/vectors.json
|
|
bip39VectorsFile :: ByteString
|
|
bip39VectorsFile = $(embedFile "tests/fixtures/bip39-vectors.json")
|
|
|
|
-- https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
|
bip32SpecFile :: ByteString
|
|
bip32SpecFile = $(embedFile "tests/fixtures/bip-0032.mediawiki")
|
|
|
|
-- | Entropy, phrase, seed with the passphrase @TREZOR@, and master @xprv@ for each English vector.
|
|
bip39Vectors :: [(ByteString, ByteString, ByteString, ByteString)]
|
|
bip39Vectors = map row $ (right (J.eitherDecodeStrict bip39VectorsFile) :: M.Map Text [[Text]]) M.! "english"
|
|
where
|
|
row [ent, phrase, seed, xprv] = (encodeUtf8 ent, encodeUtf8 phrase, encodeUtf8 seed, encodeUtf8 xprv)
|
|
row r = error $ "unexpected vector: " <> show r
|
|
|
|
bip32Sections :: [(ByteString, [ByteString])]
|
|
bip32Sections = go $ dropWhile (not . isVector) (BC.lines bip32SpecFile)
|
|
where
|
|
isVector = B.isPrefixOf "===Test vector"
|
|
go (h : rest) | isVector h = let (body, rest') = break (B.isPrefixOf "==") rest in (h, body) : go rest'
|
|
go _ = []
|
|
|
|
-- | Vectors 1 to 4: name, seed, and the chains with their @xprv@. Vector 5 has no seed.
|
|
bip32Vectors :: [(String, ByteString, [(String, [Word32], ByteString)])]
|
|
bip32Vectors = [(BC.unpack $ BC.filter (/= '=') h, seed, chains body) | (h, body) <- bip32Sections, Just seed <- [listToMaybe $ prefixed "Seed (hex): " body]]
|
|
where
|
|
chains (l : ls) | Just c <- B.stripPrefix "* Chain " l, Just prv <- listToMaybe $ prefixed "** ext prv: " (take 2 ls) = (T.unpack $ chain c, path $ chain c, prv) : chains ls
|
|
chains (_ : ls) = chains ls
|
|
chains [] = []
|
|
chain = T.replace "<sub>H</sub>" "'" . decodeLatin1
|
|
path = map component . drop 1 . T.splitOn "/"
|
|
component s = maybe (index s) (B32.hardened . index) $ T.stripSuffix "'" s
|
|
index = read . T.unpack
|
|
|
|
-- | The vector 5 keys outside @[1, n-1]@; its other entries are malformed serializations, which this library does not parse.
|
|
bip32InvalidKeys :: [(String, ByteString)]
|
|
bip32InvalidKeys =
|
|
[ (BC.unpack reason, xprv)
|
|
| (h, body) <- bip32Sections,
|
|
h == "===Test vector 5===",
|
|
e <- prefixed "* " body,
|
|
let (xprv, rest) = BC.break (== ' ') e
|
|
reason = BC.takeWhile (/= ')') $ B.drop 2 rest,
|
|
"private key" `B.isPrefixOf` reason
|
|
]
|
|
|
|
prefixed :: ByteString -> [ByteString] -> [ByteString]
|
|
prefixed p = foldr (\l acc -> maybe acc (: acc) $ B.stripPrefix p l) []
|
|
|
|
keccakTests :: Spec
|
|
keccakTests = do
|
|
it "hashes the empty string" $
|
|
toHex (C.keccak256 "") `shouldBe` "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
|
|
it "hashes abc" $
|
|
toHex (C.keccak256 "abc") `shouldBe` "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"
|
|
|
|
secp256k1Tests :: TVar ChaChaDRG -> Spec
|
|
secp256k1Tests g = do
|
|
it "derives the known address for a known key" $
|
|
(strEncode <$> addressFromPrivateKey g testKey) `shouldReturn` "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"
|
|
it "serializes a public key in both SEC1 forms" $ do
|
|
pk <- S.secp256k1PublicKey g testKey
|
|
comp <- S.serializePublicKey S.Compressed pk
|
|
uncomp <- S.serializePublicKey S.Uncompressed pk
|
|
B.length comp `shouldBe` 33
|
|
B.length uncomp `shouldBe` 65
|
|
B.head uncomp `shouldBe` 0x04
|
|
-- both forms contain the same x coordinate, and the compressed prefix encodes the parity of y
|
|
B.take 32 (B.drop 1 uncomp) `shouldBe` B.drop 1 comp
|
|
B.head comp `shouldBe` (if odd (B.last uncomp) then 0x03 else 0x02)
|
|
forM_ bip32InvalidKeys $ \(reason, xprv) ->
|
|
it ("rejects the BIP-32 vector 5 " <> reason) $
|
|
isLeft (S.mkPrivateKey (hx . fst $ xprvHex xprv)) `shouldBe` True
|
|
it "rejects a short private key" $
|
|
isLeft (S.mkPrivateKey (BA.replicate 31 1)) `shouldBe` True
|
|
it "adds a tweak to a private key" $
|
|
(fmap (toHex . S.unPrivateKey) <$> S.privateKeyTweakAdd testKey (BA.replicate 31 0 <> BA.singleton 1))
|
|
`shouldReturn` Just "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362319"
|
|
it "returns Nothing for a tweak that makes the key zero" $
|
|
(isNothing <$> S.privateKeyTweakAdd testKey (hx "b3f77c596efd6c829dceb8e4a2449df9bc5db3853ec62710db698e7291001e29"))
|
|
`shouldReturn` True
|
|
it "returns Nothing for a tweak that is not 32 bytes" $
|
|
(isNothing <$> S.privateKeyTweakAdd testKey (BA.replicate 31 1)) `shouldReturn` True
|
|
where
|
|
testKey = right $ S.mkPrivateKey (hx "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318")
|
|
|
|
bip39Tests :: TVar ChaChaDRG -> Spec
|
|
bip39Tests g = do
|
|
describe "official English vectors" $
|
|
forM_ (zip [0 :: Int ..] bip39Vectors) $ \(i, (entHex, phrase, seedHex, xprv)) ->
|
|
it ("vector " <> show i) $ do
|
|
let ent = right $ B39.mkEntropy (hx entHex)
|
|
seed = B39.entropySeed ent "TREZOR"
|
|
B39.entropyPhrase ent `shouldBe` phrase
|
|
B39.parsePhrase (decodeLatin1 phrase) `shouldBe` Right ent
|
|
toHex seed `shouldBe` seedHex
|
|
xkHex (right $ B32.masterKey seed) `shouldBe` xprvHex xprv
|
|
it "embeds the upstream wordlist, unchanged" $
|
|
-- sha256 of bitcoin/bips/bip-0039/english.txt
|
|
C.sha256Hash (BC.unlines englishWordList)
|
|
`shouldBe` hx "2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda"
|
|
it "embeds the upstream test vectors, unchanged" $ do
|
|
C.sha256Hash bip39VectorsFile `shouldBe` hx "fa3b937b7cff9c9b8ecd3aa011faeb8d6dd67993174b72326e83f4de8fdb30f8"
|
|
length bip39Vectors `shouldBe` 24
|
|
it "rejects a bad checksum" $
|
|
B39.parsePhrase "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
|
|
`shouldSatisfy` isLeft
|
|
it "rejects a word outside the list" $
|
|
B39.parsePhrase "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon simplex"
|
|
`shouldSatisfy` isLeft
|
|
it "rejects an empty phrase, counting the words" $
|
|
B39.parsePhrase "" `shouldBe` Left "Failed reading: mnemonic: expected 12, 15, 18, 21 or 24 words, got 0"
|
|
it "rejects a word with trailing punctuation, naming it" $
|
|
B39.parsePhrase "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about."
|
|
`shouldBe` Left "Failed reading: mnemonic: not in wordlist"
|
|
it "rejects a wrong word count" $
|
|
B39.parsePhrase "abandon abandon about" `shouldSatisfy` isLeft
|
|
it "accepts a capitalised phrase and normalises it" $
|
|
(B39.entropyPhrase <$> B39.parsePhrase "Abandon ABANDON abandon abandon abandon abandon abandon abandon abandon abandon abandon About")
|
|
`shouldBe` Right canonicalPhrase
|
|
it "accepts extra whitespace" $
|
|
B39.parsePhrase " abandon\tabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about "
|
|
`shouldBe` Right canonicalEntropy
|
|
it "accepts Unicode whitespace between words" $ do
|
|
let spaced sep = T.intercalate sep . T.words $ decodeLatin1 canonicalPhrase
|
|
B39.parsePhrase (spaced "\x00A0") `shouldBe` Right canonicalEntropy
|
|
B39.parsePhrase (spaced "\x3000") `shouldBe` Right canonicalEntropy
|
|
it "rejects an invalid entropy size" $
|
|
B39.mkEntropy (BA.replicate 17 0) `shouldSatisfy` isLeft
|
|
it "generates entropy whose phrase parses back" $
|
|
forM_ (zip [minBound .. maxBound] [12, 15, 18, 21, 24]) $ \(s, n) -> do
|
|
ent <- atomically $ B39.randomEntropy s g
|
|
length (BC.words $ B39.entropyPhrase ent) `shouldBe` n
|
|
B39.parsePhrase (decodeLatin1 $ B39.entropyPhrase ent) `shouldBe` Right ent
|
|
|
|
canonicalPhrase :: ByteString
|
|
canonicalPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
|
|
canonicalEntropy :: B39.WalletEntropy
|
|
canonicalEntropy = right $ B39.parsePhrase (decodeLatin1 canonicalPhrase)
|
|
|
|
bip32Tests :: TVar ChaChaDRG -> Spec
|
|
bip32Tests g = do
|
|
it "embeds the upstream specification, unchanged" $ do
|
|
C.sha256Hash bip32SpecFile `shouldBe` hx "e5e00a8289db2f681052cf24a745320afc225e66b25d1e489a7c884d2fc7f11f"
|
|
map (\(name, _, chains) -> (name, length chains)) bip32Vectors `shouldBe` [("Test vector 1", 6), ("Test vector 2", 6), ("Test vector 3", 2), ("Test vector 4", 3)]
|
|
map fst bip32InvalidKeys `shouldBe` ["private key 0 not in 1..n-1", "private key n not in 1..n-1"]
|
|
forM_ bip32Vectors $ \(name, seedHex, chains) ->
|
|
describe name $
|
|
forM_ chains $ \(chain, path, xprv) ->
|
|
it chain $ do
|
|
xk <- right <$> B32.derivePath g (right $ B32.masterKey (hx seedHex)) path
|
|
xkHex xk `shouldBe` xprvHex xprv
|
|
it "rejects a seed shorter than 16 bytes" $
|
|
isLeft (B32.masterKey (BA.replicate 15 1)) `shouldBe` True
|
|
it "rejects a seed longer than 64 bytes" $
|
|
isLeft (B32.masterKey (BA.replicate 65 1)) `shouldBe` True
|
|
it "renders a path" $
|
|
B32.renderPath [B32.hardened 44, B32.hardened 60, B32.hardened 0, 0, 0] `shouldBe` "m/44'/60'/0'/0/0"
|
|
|
|
canonicalMaster :: B32.ExtendedKey
|
|
canonicalMaster = right $ B32.masterKey (B39.entropySeed canonicalEntropy "")
|
|
|
|
derivationTests :: TVar ChaChaDRG -> Spec
|
|
derivationTests g = do
|
|
it "derives the standard BIP-39 seed" $
|
|
toHex seed
|
|
`shouldBe` "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
|
|
forM_ [(0, "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"), (1, "0x78839F6054d7ed13918bAe0473BA31b1Ca9D7265"), (2, "0x07B5FdfEB4E11826D233403Fe8Db0611CCF4c231")] $ \(i, a) ->
|
|
it ("derives the account " <> show i <> " address") $
|
|
(strEncode <$> addrAt i) `shouldReturn` a
|
|
it "derives distinct addresses for accounts 0 to 4" $
|
|
mapM addrAt [0 .. 4] >>= (`shouldSatisfy` \as -> length as == length (nub as))
|
|
it "derives an account key together with its address" $ do
|
|
let path = bip44Path Ethereum $ account 0
|
|
(xk, a) <- right <$> deriveAddress g canonicalMaster path
|
|
xk' <- right <$> B32.derivePath g canonicalMaster path
|
|
xkHex xk `shouldBe` xkHex xk'
|
|
addressFromPrivateKey g (B32.xkKey xk) `shouldReturn` a
|
|
it "renders the Ethereum path of an account" $
|
|
B32.renderPath (bip44Path Ethereum $ account 7) `shouldBe` "m/44'/60'/7'/0/0"
|
|
it "rejects an account index at or above 2^31" $ do
|
|
mkAccountIndex 0x7fffffff `shouldSatisfy` isRight
|
|
mkAccountIndex 0x80000000 `shouldSatisfy` isLeft
|
|
where
|
|
seed = B39.entropySeed canonicalEntropy ""
|
|
account = right . mkAccountIndex
|
|
addrAt i = snd . right <$> deriveAddress g canonicalMaster (bip44Path Ethereum $ account i)
|
|
|
|
eip55Tests :: Spec
|
|
eip55Tests = do
|
|
describe "spec vectors round-trip" $
|
|
forM_ specAddresses $ \a ->
|
|
it (BC.unpack a) $
|
|
strEncode (right $ strDecode @Address a) `shouldBe` a
|
|
it "accepts an all-lowercase address" $
|
|
strDecode @Address "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" `shouldSatisfy` isRight
|
|
it "accepts an all-uppercase address" $
|
|
strDecode @Address "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED" `shouldSatisfy` isRight
|
|
it "accepts an address without the 0x prefix" $
|
|
strDecode @Address "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" `shouldSatisfy` isRight
|
|
it "rejects a bad EIP-55 checksum" $
|
|
strDecode @Address "0x5aAeb6053f3E94C9b9A09f33669435E7Ef1BeAed" `shouldSatisfy` isLeft
|
|
it "rejects the wrong length" $
|
|
strDecode @Address "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAe" `shouldSatisfy` isLeft
|
|
it "rejects non-hex characters" $
|
|
strDecode @Address "0xZaAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
|
|
`shouldBe` Left "Failed reading: address: expected 40 hex digits, got 0"
|
|
it "round-trips every byte value through the checksummed form" $
|
|
forM_ everyByteAddresses $ \a ->
|
|
strDecode (strEncode a) `shouldBe` Right a
|
|
it "round-trips every byte value through the lowercase form" $
|
|
forM_ everyByteAddresses $ \a ->
|
|
strDecode (BC.map toLower (strEncode a)) `shouldBe` Right a
|
|
where
|
|
-- 13 x 20 = 260 bytes, so every value 0x00..0xff appears at least once
|
|
everyByteAddresses =
|
|
[ right . strDecode @Address . ("0x" <>) . toHex . B.pack $
|
|
[fromIntegral ((i * 20 + j) `mod` 256) | j <- [0 .. 19 :: Int]]
|
|
| i <- [0 .. 12 :: Int]
|
|
]
|
|
specAddresses =
|
|
[ "0x52908400098527886E0F7030069857D2E4169EE7",
|
|
"0x8617E340B3D01FA5F11F306F4090FD50E238070D",
|
|
"0xde709f2102306220921060314715629080e2fb77",
|
|
"0x27b1fdb04752bbc536007a920d24acb045561c26",
|
|
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
|
|
"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
|
|
"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
|
|
"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
|
|
]
|
|
|
|
rlpTests :: Spec
|
|
rlpTests = do
|
|
forM_ rlpVectors $ \(name, item, expected) ->
|
|
it ("encodes " <> name) $ toHex (rlpEncode item) `shouldBe` expected
|
|
it "encodes scalars without leading zeros" $ do
|
|
toHex (rlpEncode $ scalarItem $ B.replicate 32 0) `shouldBe` "80"
|
|
toHex (rlpEncode $ scalarItem $ hx "000f") `shouldBe` "0f"
|
|
toHex (rlpEncode $ scalarItem $ hx "000400") `shouldBe` "820400"
|
|
where
|
|
rlpVectors =
|
|
[ ("the empty string", RLPBytes "", "80"),
|
|
("the empty list", RLPList [], "c0"),
|
|
("dog", RLPBytes "dog", "83646f67"),
|
|
("[cat, dog]", RLPList [RLPBytes "cat", RLPBytes "dog"], "c88363617483646f67"),
|
|
("the set-theoretic three", RLPList [RLPList [], RLPList [RLPList []], RLPList [RLPList [], RLPList [RLPList []]]], "c7c0c1c0c3c0c1c0"),
|
|
("a 56-byte string", RLPBytes (B.replicate 56 0x61), "b838" <> B.concat (replicate 56 "61"))
|
|
]
|
|
|
|
-- expected signatures and transactions are viem's for the same key and inputs
|
|
signingTests :: TVar ChaChaDRG -> Spec
|
|
signingTests g = do
|
|
it "derives the EIP-155 example address" $
|
|
(strEncode <$> addressFromPrivateKey g txKey) `shouldReturn` "0x9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F"
|
|
it "signs a digest as viem does" $
|
|
S.signRecoverable g txKey (C.keccak256 "simplex")
|
|
`shouldReturn` Right
|
|
S.RecoverableSignature
|
|
{ S.rsR = hx "8d5aa94d4288fb8297dd9ff2c9cc5026b09b5305ace1f0b48a864ab116f2d3b0",
|
|
S.rsS = hx "3eb3b2c117d918736df192c69cef499294f2a070aff74c2bf69776a179b7cdb1",
|
|
S.rsRecId = 1
|
|
}
|
|
it "refuses a digest that is not 32 bytes" $
|
|
(isLeft <$> S.signRecoverable g txKey "digest") `shouldReturn` True
|
|
it "serializes a signature as r, s and v, as viem does" $
|
|
serializeSignature viemSig `shouldBe` hx "8d5aa94d4288fb8297dd9ff2c9cc5026b09b5305ace1f0b48a864ab116f2d3b03eb3b2c117d918736df192c69cef499294f2a070aff74c2bf69776a179b7cdb11c"
|
|
it "recovers the address that signed a digest" $
|
|
(strEncode <$$> recoverAddress viemSig digest) `shouldReturn` Right "0x9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F"
|
|
it "refuses a recovery id above 3" $
|
|
(isLeft <$> S.recoverPublicKey viemSig {S.rsRecId = 4} digest) `shouldReturn` True
|
|
it "refuses r at or above the group order" $
|
|
(isLeft <$> S.recoverPublicKey viemSig {S.rsR = B.replicate 32 0xff} digest) `shouldReturn` True
|
|
it "refuses r of zero" $
|
|
(isLeft <$> S.recoverPublicKey viemSig {S.rsR = B.replicate 32 0} digest) `shouldReturn` True
|
|
it "refuses r that is not 32 bytes" $
|
|
(isLeft <$> S.recoverPublicKey viemSig {S.rsR = B.drop 1 $ S.rsR viemSig} digest) `shouldReturn` True
|
|
it "refuses to recover from a digest that is not 32 bytes" $
|
|
(isLeft <$> S.recoverPublicKey viemSig "digest") `shouldReturn` True
|
|
where
|
|
digest = C.keccak256 "simplex"
|
|
viemSig =
|
|
S.RecoverableSignature
|
|
{ S.rsR = hx "8d5aa94d4288fb8297dd9ff2c9cc5026b09b5305ace1f0b48a864ab116f2d3b0",
|
|
S.rsS = hx "3eb3b2c117d918736df192c69cef499294f2a070aff74c2bf69776a179b7cdb1",
|
|
S.rsRecId = 1
|
|
}
|
|
|
|
transactionTests :: TVar ChaChaDRG -> Spec
|
|
transactionTests g = do
|
|
it "signs a contract call as viem does" $
|
|
(toHex <$$> signEip1559Tx g txKey contractCall)
|
|
`shouldReturn` Right "02f89383aa36a709843b9aca008506fc23ac00830186a094353535353535353535353535353535353535353580a4f14fcbc81111111111111111111111111111111111111111111111111111111111111111c001a08be78ee2e58a62763be161e3c4440c74c3b6ae56d667b143554e12239a6328e5a0790d86c0398a5295d981d12f4abc10d61b58917b7b7fd21215f7686457aafc19"
|
|
it "signs a value transfer with zero fields as viem does" $
|
|
(toHex <$$> signEip1559Tx g txKey transfer)
|
|
`shouldReturn` Right "02f86a01808001825208943535353535353535353535353535353535353535880de0b6b3a764000080c001a0d04dbcdcfa4c64fe9deb8f0fea4d7a1f07407e80fd2b066b8f6d58a9baf6af0ba074c14688697464225bbe29301db1df7f0c1b9484a52eaf76162524baba2f2791"
|
|
it "encodes every word of fields wider than 64 bits" $
|
|
(B.isInfixOf wideFields . toHex <$$> signEip1559Tx g txKey wide)
|
|
`shouldReturn` Right True
|
|
where
|
|
to = right $ strDecode "0x3535353535353535353535353535353535353535"
|
|
contractCall = Eip1559Tx {txChainId = 11155111, txNonce = 9, txMaxPriorityFeePerGas = 1000000000, txMaxFeePerGas = 30000000000, txGasLimit = 100000, txTo = to, txValue = 0, txData = hx "f14fcbc8" <> B.replicate 32 0x11}
|
|
transfer = Eip1559Tx {txChainId = 1, txNonce = 0, txMaxPriorityFeePerGas = 0, txMaxFeePerGas = 1, txGasLimit = 21000, txTo = to, txValue = 1000000000000000000, txData = ""}
|
|
wide = Eip1559Tx {txChainId = maxBound, txNonce = maxBound - 1, txMaxPriorityFeePerGas = 0x10000000000000000000000007, txMaxFeePerGas = 0x80000000000000010000000000000003, txGasLimit = 0x10000000001, txTo = to, txValue = 0x8000000000000001000000000000000100000000000000010000000000000009, txData = ""}
|
|
wideFields = "88ffffffffffffffff88fffffffffffffffe8d10000000000000000000000007908000000000000001000000000000000386010000000001943535353535353535353535353535353535353535a0800000000000000100000000000000010000000000000001000000000000000980c0"
|
|
|
|
txKey :: S.Secp256k1PrivateKey
|
|
txKey = right $ S.mkPrivateKey $ BA.replicate 32 0x46
|
|
|
|
eip712Tests :: Spec
|
|
eip712Tests =
|
|
it "hashes typed data as viem does" $
|
|
toHex (hashTypedData domain registerType members)
|
|
`shouldBe` "94b056be18ea1791fc4960d6dce87e715301634d7402fb1223f536749d20dff2"
|
|
where
|
|
domain = Eip712Domain {edName = "SimpleX", edVersion = "2", edChainId = 11155111, edVerifyingContract = address}
|
|
registerType = "RegisterName(string label,address owner,address resolver,bytes[] data,uint256 deadline)"
|
|
members = [VBytes "simplex", VAddress address, VAddress address, VArray [VBytes "\x01\x02", VBytes $ B.replicate 40 0xff], VUint 4102444800]
|
|
address = right $ strDecode "0x3535353535353535353535353535353535353535"
|