store: add reads and calldata for record edits

This commit is contained in:
shum committed 2026-10-22 02:15:38 +00:00
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]]
+44 -7
View File
@@ -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