Files
simplex-chat/tests/Bots/StoreService/NamesSignatureTests.hs
T

192 lines
12 KiB
Haskell

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Bots.StoreService.NamesSignatureTests (namesSignatureTests) where
import Control.Monad (forM_)
import Data.ByteArray.Encoding (Base (Base16), convertFromBase)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Word (Word64)
import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature (..))
import Simplex.Messaging.Encoding.String (strDecode)
import Simplex.Messaging.Eth.Address (Address, recoverAddress)
import Simplex.Messaging.SimplexName (SimplexTLD (..), labelHash)
import StoreService.Names.Abi
import StoreService.Names.Phases (RegistrationId (..))
import StoreService.Names.Registration
import StoreService.Names.Signatures
import Test.Hspec
namesSignatureTests :: Spec
namesSignatureTests = describe "StoreService names: signatures" $ do
-- expected values are viem 2.57.5's for the fixtures below, the webapp's wallet (BIP-39 entropy 6f2d1c9a4b7e3f80d15a2c6e9b04f7a3, account 0)
-- and the SimplexSponsor, SimplexController, SimplexResolver and SimplexPriceOracle ABIs of ens-contracts PR #36 at 23892cc
describe "EIP-712" $ do
it "commits to a registration with one contact link" $
makeCommitment dynamis `shouldBe` commitment
it "hashes RegisterName" $
registerIntentDigest (sponsorDomain chainId sponsor) dynamis 0 1800000000 `shouldBe` hx "0x4b0bcc282db581446f6c25e0e3fbe2839409acbc011bb3e082523dd9eab85a6a"
it "hashes SetText" $
setTextDigest (resolverDomain chainId resolver) (namehash TLDSimplex "dynamis") "simplex.contact" "https://x" 3 99 `shouldBe` hx "0xa62d4320889eed3713462d725dc298cd96885572b8e6b424e93a205a96b96fb5"
it "hashes ClearRecords" $
clearRecordsDigest (resolverDomain chainId resolver) (namehash TLDSimplex "dynamis") 4 99 `shouldBe` hx "0xca4bd243288f27f38e3b45384ba36ce685ff155352cf8125f031e6fb189a46dd"
describe "EIP-191 messages" $ do
it "signs an order" $
testMessage
(orderMessage dynamisOrder timestamp)
"simplex.domains order 0x78b602f03f164366531da963764c1f2fdfd588d0487daf2aa261ca799e5ffe33 7 69259500 0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1 0xb1971beb567c8dc1b2f01f0d823754c6168d8d6830db2221702a7678e1804674 1792574100"
"0x5deee1b3589df9e53665e34aa65a491a4eabfd76a39207b0e9303457f7c44b7a"
"0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a84df9ffdccc39c5cb827cde4d846c7662f591665f1b868ab1c15778646d3f414e1b"
it "signs find with the owner in lowercase" $
testMessage
(findMessage owner timestamp)
"simplex.domains find 0x014f9c09cceedef1b713110d04d3a07d5b5d9b73 1792574100"
"0x08a989fbf0457984c062b44d7237be9c7948f2cf23d48410609777a09b515646"
"0xfa0908260c3f21c8e47bd37e2c3cf3bd6bc62966591ab1a2c4e31695646548f47d2e79b34413be27b4b07330682de642f48a8f5e641449d7d79728eeb4ab14da1c"
it "signs another name for an order, with its duration" $
testMessage
(retargetMessage registrationId dynamisOrder {ofLabelHash = labelHash "another"} timestamp)
"simplex.domains name t2Jk8RmQ4vXw9NcP3yLb6g 0x5bfbbf75311ec002834031d73fa30bfe8bdf675f4a3df72443841490c4b49d02 7 69259500 0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1 0xb1971beb567c8dc1b2f01f0d823754c6168d8d6830db2221702a7678e1804674 1792574100"
"0x8bc79aa98fbd53013e8961db27554ecd17139e14ec168b86afbaf2fc910bfc52"
"0xe3d680bcc62255179d5260beeb920319c86bd919f6b01927bee01509cf6c1fe3121151947fb9e3a310e6652c0c9a9bf7ceb48e8d903774a38a3162d916e8b4591b"
it "signs a taken order's code, with its length" $
testMessage
(takenCodeMessage registrationId "7hg6ED6EotRVm21uS1KOZ-CKEzkJK5hvG4W5atfcn_Q" 7 timestamp)
"simplex.domains code t2Jk8RmQ4vXw9NcP3yLb6g 7hg6ED6EotRVm21uS1KOZ-CKEzkJK5hvG4W5atfcn_Q 7 1792574100"
"0x1f61297cd585c7c94218f8f87f30ea20e9b0631d36890460f24ee9bca83a2a19"
"0x897173292eb63ac3587d3c900af08dee71f296af8718ec045dd49ce953bb0f666eefe546bea0d3623022c7cc7f03cc572a498bb0ceac26bb46ed37c9e99ecdb51b"
describe "signature format" $ do
it "refuses a high s, though recovery accepts it" testHighS
it "refuses v other than 27 or 28" $
forM_ ["00", "01", "1d"] $ \v -> parseSignature (T.dropEnd 2 orderSignature <> v) `shouldBe` Left SERecoveryId
it "refuses uppercase hex, no 0x and a wrong length" $ do
parseSignature ("0x" <> T.toUpper (T.drop 2 orderSignature)) `shouldBe` Left SEFormat
parseSignature (T.drop 2 orderSignature) `shouldBe` Left SEFormat
parseSignature (T.dropEnd 2 orderSignature) `shouldBe` Left SEFormat
parseSignature (orderSignature <> "00") `shouldBe` Left SEFormat
describe "addresses" $ do
it "accepts the EIP-55 form in orders" $
parseChecksumAddress ownerText `shouldBe` Right owner
it "refuses a letter in the wrong case and one-case hex in orders" $ do
parseChecksumAddress "0x014F9c09CcEeDEF1B713110d04D3a07d5B5D9b73" `shouldBe` Left ADChecksum
parseChecksumAddress ownerLower `shouldBe` Left ADChecksum
parseChecksumAddress ownerUpper `shouldBe` Left ADChecksum
it "accepts any case in find" $
forM_ [ownerText, ownerLower, ownerUpper, "0x014F9c09CcEeDEF1B713110d04D3a07d5B5D9b73"] $ \a ->
parseAnyCaseAddress a `shouldBe` Right owner
it "refuses what is not an address" $
forM_ ["014f9c09cceedef1b713110d04d3a07d5b5d9b73", "0X014f9c09cceedef1b713110d04d3a07d5b5d9b73", T.dropEnd 1 ownerLower, ownerLower <> "0", "0x014f9c09cceedef1b713110d04d3a07d5b5d9b7g"] $ \a -> do
parseAnyCaseAddress a `shouldBe` Left ADFormat
parseChecksumAddress a `shouldBe` Left ADFormat
describe "ABI return data" $ do
it "decodes each type as viem encodes it" $ do
decodeUint256 (hx "00000000000000000000000000000000000000018ee90ff6c373e0ee4e3f0ad2") `shouldBe` Right 123456789012345678901234567890
decodeAddress (hx "000000000000000000000000014f9c09cceedef1b713110d04d3a07d5b5d9b73") `shouldBe` Right owner
decodeBool (hx "0000000000000000000000000000000000000000000000000000000000000001") `shouldBe` Right True
decodeBool (B.replicate 32 0) `shouldBe` Right False
decodeBytes32 (hx "833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb") `shouldBe` Right node
decodeString retString `shouldBe` Right "https://smp8.simplex.im/a#contact"
decodeString (hx "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000") `shouldBe` Right ""
decodeBytes (hx "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010410f13a8c833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000f73696d706c65782e636f6e746163740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002168747470733a2f2f736d70382e73696d706c65782e696d2f6123636f6e746163740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") `shouldBe` Right (hx "10f13a8c833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000f73696d706c65782e636f6e746163740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002168747470733a2f2f736d70382e73696d706c65782e696d2f6123636f6e7461637400000000000000000000000000000000000000000000000000000000000000")
it "decodes the oracle's prices" $ do
decodePrices retPrices `shouldBe` Right (5000, [(7, 10000), (6, 20000)])
decodePrices (hx "000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000") `shouldBe` Right (5000, [])
it "refuses data that does not hold the type" $ do
decodeUint256 (B.replicate 31 0) `shouldBe` Left AEShort
decodeAddress (B.singleton 1 <> B.drop 1 (hx "000000000000000000000000014f9c09cceedef1b713110d04d3a07d5b5d9b73")) `shouldBe` Left AENotAddress
decodeBool (uint256 (2 :: Int)) `shouldBe` Left AENotBool
decodeString (uint256 (4096 :: Int) <> B.drop 32 retString) `shouldBe` Left AEOffset
decodeString (B.take 80 retString) `shouldBe` Left AEShort
decodeString (abiTuple [Dynamic $ abiBytes "\xff"]) `shouldBe` Left AENotUtf8
decodePrices (B.dropEnd 32 retPrices) `shouldBe` Left AEShort
testMessage :: ByteString -> ByteString -> Text -> Text -> IO ()
testMessage msg expected digest signature = do
msg `shouldBe` expected
personalDigest msg `shouldBe` hx digest
sig <- either (fail . show) pure $ parseSignature signature
recoverAddress sig (personalDigest msg) `shouldReturn` Right owner
testHighS :: IO ()
testHighS = do
parseSignature highS `shouldBe` Left SEHighS
let (rsR, (rsS, v)) = B.splitAt 32 <$> B.splitAt 32 (hx highS)
recoverAddress RecoverableSignature {rsR, rsS, rsRecId = B.head v - 27} (personalDigest $ orderMessage dynamisOrder timestamp)
`shouldReturn` Right owner
where
highS = "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a8b206002333c63a347d8321b27b93899bc51d768793c21589fe7ae62862f6fff31c"
-- fixtures
dynamis :: Registration
dynamis =
Registration
{ rLabel = "dynamis",
rTLD = TLDSimplex,
rOwner = owner,
rDuration = duration,
rSecret = B.replicate 32 0x11,
rResolver = resolver,
rContactLink = Just "https://smp8.simplex.im/a#contact",
rChannelLink = Nothing
}
dynamisOrder :: OrderFields
dynamisOrder =
OrderFields
{ ofLabelHash = labelHash "dynamis",
ofLength = 7,
ofDuration = duration,
ofCommitment = commitment,
ofSealed = "q3Zk0Yx7c2VhbGVkLW9yZGVyLWZpeHR1cmU"
}
commitment :: ByteString
commitment = hx "0x9cf04ddef2eedfb02e6e7ba58f879c0636c584b6acc67ffe9f97a06bfb016ac1"
node :: ByteString
node = hx "0x833cc73a7eae1ddee67ec8845ec3d39799406c7bc87fab7c504a7e0ab5cb89bb"
-- 21 October 2026 09:15 UTC
timestamp :: Word64
timestamp = 1792574100
-- 2 years of 365 days and the free days until 1 January 2027
duration :: Word64
duration = 69259500
registrationId :: RegistrationId
registrationId = RegistrationId "t2Jk8RmQ4vXw9NcP3yLb6g"
orderSignature :: Text
orderSignature = "0x73e32be461e99e64b093a4ad09b04b4d810e5623a1b0e9c9fdb4701ec2a6a2a84df9ffdccc39c5cb827cde4d846c7662f591665f1b868ab1c15778646d3f414e1b"
retString :: ByteString
retString = hx "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002168747470733a2f2f736d70382e73696d706c65782e696d2f6123636f6e7461637400000000000000000000000000000000000000000000000000000000000000"
retPrices :: ByteString
retPrices = hx "0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000004e20"
owner :: Address
owner = either error id $ strDecode $ encodeUtf8 ownerText
ownerText, ownerLower, ownerUpper :: Text
ownerText = "0x014f9c09CcEeDEF1B713110d04D3a07d5B5D9b73"
ownerLower = T.toLower ownerText
ownerUpper = "0x" <> T.toUpper (T.drop 2 ownerText)
sponsor, resolver :: Address
sponsor = either error id $ strDecode "0x9a676e781a523b5d0c0e43731313a708cb607508"
resolver = either error id $ strDecode "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6"
chainId :: Word64
chainId = 31337
hx :: Text -> ByteString
hx t = either error id $ convertFromBase Base16 $ encodeUtf8 $ fromMaybe t $ T.stripPrefix "0x" t