mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-12 18:08:13 +00:00
store: add reads and calldata for record edits
This commit is contained in:
1 parent
c7e6f891d2
commit
c03bf3f80a
7 files changed
+106
-15
No files matched your search
@@ -17,6 +17,7 @@ module StoreService.Names.Chain
|
||||
newChainClient,
|
||||
ethCall,
|
||||
latestBlock,
|
||||
blockAt,
|
||||
transactionReceipt,
|
||||
transactionByHash,
|
||||
transactionCount,
|
||||
@@ -50,7 +51,7 @@ import Network.HTTP.Client (Manager)
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import Network.HTTP.Client.TLS (newTlsManager)
|
||||
import Network.HTTP.Types (hContentType, methodPost, statusCode, statusIsSuccessful)
|
||||
import Numeric (readHex)
|
||||
import Numeric (readHex, showHex)
|
||||
import Simplex.Messaging.Crypto (keccak256)
|
||||
import Simplex.Messaging.Eth.Address (Address, addressFromPrivateKey)
|
||||
import Simplex.Messaging.Eth.RLP (RLPItem (..))
|
||||
@@ -135,8 +136,15 @@ ethCall :: ChainClient -> Address -> ByteString -> IO (Either ChainError ByteStr
|
||||
ethCall c to calldata = rpc c "eth_call" [J.object ["to" .= to, "data" .= hex calldata], "latest"] bytes
|
||||
|
||||
latestBlock :: ChainClient -> IO (Either ChainError Block)
|
||||
latestBlock c =
|
||||
rpc c "eth_getBlockByNumber" ["latest", J.Bool False] $ J.withObject "block" $ \b ->
|
||||
latestBlock c = blockByNumber c "latest"
|
||||
|
||||
-- | A block the node does not have is a failed read.
|
||||
blockAt :: ChainClient -> Word64 -> IO (Either ChainError Block)
|
||||
blockAt c number = blockByNumber c $ J.String $ "0x" <> T.pack (showHex number "")
|
||||
|
||||
blockByNumber :: ChainClient -> J.Value -> IO (Either ChainError Block)
|
||||
blockByNumber c tag =
|
||||
rpc c "eth_getBlockByNumber" [tag, J.Bool False] $ J.withObject "block" $ \b ->
|
||||
Block <$> (quantity =<< b .: "number") <*> (quantity =<< b .: "timestamp") <*> (quantity =<< b .: "baseFeePerGas")
|
||||
|
||||
-- | Nothing while the transaction is not mined, or after the node pruned its receipt.
|
||||
|
||||
@@ -12,6 +12,7 @@ module StoreService.Names.Contracts
|
||||
secondsPerYear,
|
||||
nameStatus,
|
||||
nameRecords,
|
||||
textRecord,
|
||||
chainPriceCents,
|
||||
codeLengthFor,
|
||||
sponsorNonce,
|
||||
@@ -115,11 +116,13 @@ nameRecords ct lh RegisteredName {rnOwner, rnResolver}
|
||||
| otherwise =
|
||||
withinDeadline ct "name records" $ together $
|
||||
(\rdContact rdChannel rdNonce -> Just NameRecords {rdContact, rdChannel, rdNonce})
|
||||
<$> textRecord contactRecordKey
|
||||
<*> textRecord channelRecordKey
|
||||
<$> concurrent (textRecord ct lh contactRecordKey)
|
||||
<*> concurrent (textRecord ct lh channelRecordKey)
|
||||
<*> concurrent (resolverNonce ct rnOwner)
|
||||
where
|
||||
textRecord key = concurrent $ call ct (resolver ct) ("text(bytes32,string)", abiTuple [Static $ nodeArg ct lh, Dynamic $ abiBytes key]) decodeString
|
||||
|
||||
-- | The default resolver's record, "" when unset.
|
||||
textRecord :: Contracts -> LabelHash -> ByteString -> IO (Either ChainError Text)
|
||||
textRecord ct lh key = call ct (resolver ct) ("text(bytes32,string)", abiTuple [Static $ nodeArg ct lh, Dynamic $ abiBytes key]) decodeString
|
||||
|
||||
-- | Cents for a label of this length and these years. The oracle prices by length only,
|
||||
-- so a placeholder label of that length keeps the label from the RPC node.
|
||||
|
||||
@@ -19,6 +19,8 @@ module StoreService.Names.Registration
|
||||
verifyRegisterIntent,
|
||||
setTextDigest,
|
||||
clearRecordsDigest,
|
||||
setTextWithSigCalldata,
|
||||
clearRecordsWithSigCalldata,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -124,6 +126,23 @@ clearRecordsDigest domain node nonce deadline =
|
||||
"ClearRecords(bytes32 node,uint256 nonce,uint256 deadline)"
|
||||
[bytes32 node, VUint $ fromIntegral nonce, VUint $ fromIntegral deadline]
|
||||
|
||||
setTextWithSigCalldata :: Node -> Text -> Text -> Word64 -> Word64 -> RecoverableSignature -> ByteString
|
||||
setTextWithSigCalldata (Node node) key value nonce deadline sig =
|
||||
selector "setTextWithSig(bytes32,string,string,uint256,uint256,bytes)"
|
||||
<> abiTuple
|
||||
[ Static node,
|
||||
Dynamic $ abiBytes $ encodeUtf8 key,
|
||||
Dynamic $ abiBytes $ encodeUtf8 value,
|
||||
Static $ uint256 nonce,
|
||||
Static $ uint256 deadline,
|
||||
Dynamic $ abiBytes $ serializeSignature sig
|
||||
]
|
||||
|
||||
clearRecordsWithSigCalldata :: Node -> Word64 -> Word64 -> RecoverableSignature -> ByteString
|
||||
clearRecordsWithSigCalldata (Node node) nonce deadline sig =
|
||||
selector "clearRecordsWithSig(bytes32,uint256,uint256,bytes)"
|
||||
<> abiTuple [Static node, Static $ uint256 nonce, Static $ uint256 deadline, Dynamic $ abiBytes $ serializeSignature sig]
|
||||
|
||||
bytes32 :: Node -> Eip712Value
|
||||
bytes32 (Node node) = VUint $ fromInteger $ os2ip node
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module StoreService.Names.Revert (RevertName (..), decodeRevert) where
|
||||
module StoreService.Names.Revert (RevertName (..), decodeRevert, revertText) where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import StoreService.Names.Abi (selector)
|
||||
|
||||
-- | The custom errors of SimplexController, SimplexSponsor, BaseRegistrarImplementation and SimplexResolver.
|
||||
@@ -107,6 +110,10 @@ revertSignature = \case
|
||||
RNZeroAddress -> Just "ZeroAddress()"
|
||||
RNWithoutData -> Nothing
|
||||
|
||||
-- | The error's name as the contracts declare it, Nothing for a revert without data.
|
||||
revertText :: RevertName -> Maybe Text
|
||||
revertText = fmap (decodeLatin1 . BC.takeWhile (/= '(')) . revertSignature
|
||||
|
||||
revertSelectors :: Map ByteString RevertName
|
||||
revertSelectors = M.fromList [(selector signature, n) | n <- [minBound .. maxBound], Just signature <- [revertSignature n]]
|
||||
|
||||
|
||||
@@ -13,15 +13,19 @@ module Bots.StoreService.FakeEthNode
|
||||
clearFault,
|
||||
setBlock,
|
||||
setFees,
|
||||
revertDryRun,
|
||||
mine,
|
||||
dropTransaction,
|
||||
pruneReceipt,
|
||||
useNextNonce,
|
||||
sentTransactions,
|
||||
awaitSent,
|
||||
revertFault,
|
||||
fakeChainId,
|
||||
fakeController,
|
||||
fakeSponsor,
|
||||
fakeBaseRegistrar,
|
||||
fakeResolver,
|
||||
fakeRegistrarKey,
|
||||
fakeGas,
|
||||
fakeTip,
|
||||
@@ -48,7 +52,7 @@ import Data.Word (Word64)
|
||||
import Network.HTTP.Types (badGateway502, badRequest400, hContentType, ok200)
|
||||
import Network.Wai (Application, responseLBS, strictRequestBody)
|
||||
import qualified Network.Wai.Handler.Warp as Warp
|
||||
import Numeric (showHex)
|
||||
import Numeric (readHex, showHex)
|
||||
import Simplex.Messaging.Crypto (keccak256)
|
||||
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Messaging.Encoding.String (strDecode)
|
||||
@@ -84,9 +88,13 @@ data FakeState = FakeState
|
||||
-- | the transactions the node knows and has not mined, by nonce
|
||||
fsPool :: Map Word64 ByteString,
|
||||
fsReceipts :: Map ByteString (Bool, Word64),
|
||||
-- | the revert data of the dry runs of these calldata
|
||||
fsDryRuns :: Map ByteString ByteString,
|
||||
-- | the key's transaction count at the latest block
|
||||
fsUsed :: Word64,
|
||||
fsBlock :: (Word64, Word64),
|
||||
-- | the times of the blocks mined or set, by number
|
||||
fsBlockTimes :: Map Word64 Word64,
|
||||
-- | the suggested tip and the base fee
|
||||
fsFees :: (Word64, Word64),
|
||||
fsClosed :: Bool
|
||||
@@ -110,7 +118,7 @@ fakeResolver = address "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6"
|
||||
fakeRegistrarKey :: S.Secp256k1PrivateKey
|
||||
fakeRegistrarKey = either error id $ S.mkPrivateKey $ BA.replicate 32 0x46
|
||||
|
||||
-- | 2026-01-01, block 1; each mined block is this many seconds later.
|
||||
-- | 2026-01-01, block 1 after the genesis block 0; each mined block is this many seconds later.
|
||||
genesisTime, blockSeconds :: Word64
|
||||
genesisTime = 1767225600
|
||||
blockSeconds = 12
|
||||
@@ -125,8 +133,10 @@ withFakeEthNode action = do
|
||||
fsSent = [],
|
||||
fsPool = M.empty,
|
||||
fsReceipts = M.empty,
|
||||
fsDryRuns = M.empty,
|
||||
fsUsed = 0,
|
||||
fsBlock = (1, genesisTime),
|
||||
fsBlockTimes = M.fromList [(0, genesisTime - blockSeconds), (1, genesisTime)],
|
||||
fsFees = (fakeTip, fakeBaseFee),
|
||||
fsClosed = False
|
||||
}
|
||||
@@ -164,7 +174,11 @@ revertFault = RpcError 3 "execution reverted" . Just
|
||||
|
||||
-- | Sets the latest block's number and time.
|
||||
setBlock :: FakeEthNode -> Word64 -> Word64 -> IO ()
|
||||
setBlock FakeEthNode {fnState} number time = atomically $ modifyTVar' fnState $ \s -> s {fsBlock = (number, time)}
|
||||
setBlock FakeEthNode {fnState} number time = atomically $ modifyTVar' fnState $ \s -> s {fsBlock = (number, time), fsBlockTimes = M.insert number time $ fsBlockTimes s}
|
||||
|
||||
-- | The dry run of this calldata reverts with this data, or passes again with Nothing.
|
||||
revertDryRun :: FakeEthNode -> ByteString -> Maybe ByteString -> IO ()
|
||||
revertDryRun FakeEthNode {fnState} calldata revertData = atomically $ modifyTVar' fnState $ \s -> s {fsDryRuns = M.alter (const revertData) calldata $ fsDryRuns s}
|
||||
|
||||
setFees :: FakeEthNode -> Word64 -> Word64 -> IO ()
|
||||
setFees FakeEthNode {fnState} tip baseFee = atomically $ modifyTVar' fnState $ \s -> s {fsFees = (tip, baseFee)}
|
||||
@@ -177,7 +191,7 @@ mine FakeEthNode {fnState} txs = atomically $ do
|
||||
let number' = number + 1
|
||||
mined = M.fromList [(h, (ok, number')) | (h, ok) <- txs]
|
||||
used = maximum $ fsUsed : [nonce + 1 | (nonce, raw) <- M.toList fsPool, keccak256 raw `M.member` mined]
|
||||
writeTVar fnState s {fsBlock = (number', time + blockSeconds), fsReceipts = M.union mined fsReceipts, fsPool = M.dropWhileAntitone (< used) fsPool, fsUsed = used}
|
||||
writeTVar fnState s {fsBlock = (number', time + blockSeconds), fsBlockTimes = M.insert number' (time + blockSeconds) $ fsBlockTimes s, fsReceipts = M.union mined fsReceipts, fsPool = M.dropWhileAntitone (< used) fsPool, fsUsed = used}
|
||||
pure number'
|
||||
|
||||
-- | The node forgets a transaction it has not mined, as after its own restart.
|
||||
@@ -196,6 +210,13 @@ useNextNonce FakeEthNode {fnState} = atomically $ modifyTVar' fnState $ \s@FakeS
|
||||
sentTransactions :: FakeEthNode -> IO [ByteString]
|
||||
sentTransactions FakeEthNode {fnState} = reverse . fsSent <$> readTVarIO fnState
|
||||
|
||||
-- | Waits until the node has received at least this many raw transactions, and returns them in the order they were sent.
|
||||
awaitSent :: FakeEthNode -> Int -> IO [ByteString]
|
||||
awaitSent FakeEthNode {fnState} n = atomically $ do
|
||||
sent <- fsSent <$> readTVar fnState
|
||||
check $ length sent >= n
|
||||
pure $ reverse sent
|
||||
|
||||
fakeApp :: TVar FakeState -> Application
|
||||
fakeApp st req respond = do
|
||||
body <- strictRequestBody req
|
||||
@@ -219,7 +240,7 @@ fakeApp st req respond = do
|
||||
|
||||
answer :: TVar FakeState -> Text -> [J.Value] -> STM [JT.Pair]
|
||||
answer st method params = do
|
||||
s@FakeState {fsCalls, fsSent, fsPool, fsReceipts, fsUsed, fsBlock = (number, time), fsFees = (tip, baseFee)} <- readTVar st
|
||||
s@FakeState {fsCalls, fsSent, fsPool, fsReceipts, fsDryRuns, fsUsed, fsBlock = (number, time), fsBlockTimes, fsFees = (tip, baseFee)} <- readTVar st
|
||||
case (method, params) of
|
||||
("eth_call", [call, _])
|
||||
| Just (to, calldata) <- JT.parseMaybe callTarget call ->
|
||||
@@ -227,13 +248,21 @@ answer st method params = do
|
||||
Just (Returns ret) -> result $ hex ret
|
||||
Just (Reverts revertData) -> rpcError 3 "execution reverted" (Just revertData)
|
||||
Nothing -> rpcError (-32000) "no fake answer for this call" Nothing
|
||||
("eth_estimateGas", call : _)
|
||||
| Just (_, calldata) <- JT.parseMaybe callTarget call,
|
||||
Just revertData <- M.lookup calldata fsDryRuns ->
|
||||
pure $ rpcError 3 "execution reverted" (Just revertData)
|
||||
("eth_estimateGas", _) -> pure $ result $ quantity fakeGas
|
||||
("eth_getTransactionCount", [_, J.String "latest"]) -> pure $ result $ quantity $ toInteger fsUsed
|
||||
("eth_getTransactionCount", [_, J.String "pending"]) ->
|
||||
pure $ result $ quantity $ toInteger $ fsUsed + fromIntegral (length $ takeWhile (`M.member` fsPool) [fsUsed ..])
|
||||
("eth_maxPriorityFeePerGas", _) -> pure $ result $ quantity $ toInteger tip
|
||||
("eth_getBlockByNumber", [J.String "latest", _]) ->
|
||||
pure $ result $ J.object ["number" .= quantity (toInteger number), "timestamp" .= quantity (toInteger time), "baseFeePerGas" .= quantity (toInteger baseFee)]
|
||||
("eth_getBlockByNumber", [J.String "latest", _]) -> pure $ result $ blockJson baseFee number time
|
||||
-- a block not mined or set here has the time of the last one before it
|
||||
("eth_getBlockByNumber", [J.String n, _])
|
||||
| Just at <- fromQuantity n -> pure $ result $ case M.lookupLE at fsBlockTimes of
|
||||
Just (_, t) | at <= number -> blockJson baseFee at t
|
||||
_ -> J.Null
|
||||
("eth_getTransactionReceipt", [J.String h]) | Just hash <- fromHex h -> pure $ result $ case M.lookup hash fsReceipts of
|
||||
Just (ok, block) -> J.object ["transactionHash" .= h, "status" .= quantity (if ok then 1 else 0), "blockNumber" .= quantity (toInteger block)]
|
||||
Nothing -> J.Null
|
||||
@@ -260,12 +289,20 @@ answer st method params = do
|
||||
result v = ["result" .= v]
|
||||
callTarget = J.withObject "call" $ \o -> (,) <$> o .: "to" <*> (maybe (fail "not hex data") pure . fromHex =<< o .: "data")
|
||||
|
||||
blockJson :: Word64 -> Word64 -> Word64 -> J.Value
|
||||
blockJson baseFee number time = J.object ["number" .= quantity (toInteger number), "timestamp" .= quantity (toInteger time), "baseFeePerGas" .= quantity (toInteger baseFee)]
|
||||
|
||||
rpcError :: Int -> Text -> Maybe ByteString -> [JT.Pair]
|
||||
rpcError code message revertData = ["error" .= J.object (["code" .= code, "message" .= message] <> maybe [] (\d -> ["data" .= hex d]) revertData)]
|
||||
|
||||
quantity :: Integer -> Text
|
||||
quantity n = "0x" <> T.pack (showHex n "")
|
||||
|
||||
fromQuantity :: Text -> Maybe Word64
|
||||
fromQuantity t = case T.stripPrefix "0x" t of
|
||||
Just h | [(n, "")] <- readHex (T.unpack h) -> Just n
|
||||
_ -> Nothing
|
||||
|
||||
fromHex :: Text -> Maybe ByteString
|
||||
fromHex t = T.stripPrefix "0x" t >>= eitherToMaybe . convertFromBase Base16 . encodeUtf8
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namesChainTests = describe "StoreService names: chain" $ do
|
||||
g <- runIO C.newRandom
|
||||
describe "reverts" $ do
|
||||
it "names every contract error by the selector viem encodes it with" testRevertSelectors
|
||||
it "writes a revert's name as the contracts declare it, none for a revert without data" testRevertText
|
||||
it "reads a dry run's revert with data by its name" $ testDryRunRevertNamed g
|
||||
it "reads an eth_call revert with data by its name" $ testCallRevertNamed g
|
||||
it "reads ownerOf of a name nobody holds as a revert" $ testOwnerOfRevert g
|
||||
@@ -46,6 +47,7 @@ namesChainTests = describe "StoreService names: chain" $ do
|
||||
describe "reads" $ do
|
||||
it "answers eth_call from the node by contract and calldata" $ testEthCall g
|
||||
it "reads the latest block's number, time and base fee" $ testLatestBlock g
|
||||
it "reads a block by its number, and one the node does not have as a failed read" $ testBlockAt g
|
||||
it "follows a transaction from sent to mined with its receipt and the nonces" $ testFollowTransaction g
|
||||
describe "transactions" $ do
|
||||
it "signs a transaction whose hash is the node's keccak256 of the raw bytes" $ testSignedHash g
|
||||
@@ -106,6 +108,12 @@ testRevertSelectors = do
|
||||
decodeRevert (B.take 3 $ unhex "74480cc9") `shouldBe` Nothing
|
||||
decodeRevert "" `shouldBe` Just RNWithoutData
|
||||
|
||||
testRevertText :: IO ()
|
||||
testRevertText = do
|
||||
revertText RNInvalidNonce `shouldBe` Just "InvalidNonce"
|
||||
revertText RNSignatureExpired `shouldBe` Just "SignatureExpired"
|
||||
revertText RNWithoutData `shouldBe` Nothing
|
||||
|
||||
testDryRunRevertNamed :: TVar ChaChaDRG -> IO ()
|
||||
testDryRunRevertNamed g = withChain g $ \node c -> do
|
||||
setFault node "eth_estimateGas" $ revertFault insufficientAllowance
|
||||
@@ -224,6 +232,14 @@ testLatestBlock g = withChain g $ \node c -> do
|
||||
setBlock node 21000000 1798761600
|
||||
latestBlock c `shouldReturn` Right Block {blockNumber = 21000000, blockTimestamp = 1798761600, blockBaseFee = fakeBaseFee}
|
||||
|
||||
testBlockAt :: TVar ChaChaDRG -> IO ()
|
||||
testBlockAt g = withChain g $ \node c -> do
|
||||
setBlock node 100 1798761600
|
||||
mined <- mine node []
|
||||
blockAt c 100 `shouldReturn` Right Block {blockNumber = 100, blockTimestamp = 1798761600, blockBaseFee = fakeBaseFee}
|
||||
blockAt c mined `shouldReturn` Right Block {blockNumber = 101, blockTimestamp = 1798761612, blockBaseFee = fakeBaseFee}
|
||||
blockAt c 102 >>= (`shouldSatisfy` \case Left (ChainUnavailable _) -> True; _ -> False)
|
||||
|
||||
testFollowTransaction :: TVar ChaChaDRG -> IO ()
|
||||
testFollowTransaction g = withChain g $ \node c@ChainClient {ccRegistrar} -> do
|
||||
let unknown = TxHash $ B.replicate 32 0xee
|
||||
|
||||
@@ -122,6 +122,7 @@ testRecords g = withContracts g $ \node ct -> do
|
||||
answerCall node resolver noncesCall $ Returns $ uint256 (3 :: Int)
|
||||
nameRecords ct alice (registeredAlice resolver $ now + year)
|
||||
`shouldReturn` Right (Just NameRecords {rdContact = "https://smp8.simplex.im/a#Y8m3", rdChannel = "", rdNonce = 3})
|
||||
textRecord ct alice "simplex.contact" `shouldReturn` Right "https://smp8.simplex.im/a#Y8m3"
|
||||
|
||||
testOtherResolver :: TVar ChaChaDRG -> IO ()
|
||||
testOtherResolver g = withContracts g $ \_ ct -> do
|
||||
|
||||
Reference in new issue
Block a user