@@ -19,10 +19,13 @@ module EVM.Fetch
1919 , saveCache
2020 , RPCContract (.. )
2121 , makeContractFromRPC
22- -- Below 3 are needed for Echidna
22+ -- Below 4 are needed for Echidna
2323 , fetchSlotWithSession
2424 , fetchSlotWithCache
2525 , fetchWithSession
26+ , getCacheState
27+ , FetchStatus (.. )
28+ , FetchResult (.. )
2629 ) where
2730
2831import Prelude hiding (Foldable (.. ))
@@ -46,15 +49,16 @@ import qualified Data.ByteString.Lazy as BSL
4649import Data.Bifunctor (first )
4750import Control.Exception (try , SomeException )
4851
49- import Control.Monad.Trans.Maybe
5052import Data.Aeson hiding (Error )
5153import Data.Aeson.Optics
5254import Data.ByteString qualified as BS
5355import Data.Text (Text , unpack , pack )
5456import Data.Text qualified as T
5557import Data.Foldable (Foldable (.. ))
5658import Data.Map.Strict qualified as Map
57- import Data.Maybe (fromMaybe , isJust , fromJust , isNothing )
59+ import Data.Maybe (fromMaybe , fromJust , isNothing )
60+ import Data.Set qualified as Set
61+ import Data.Set (Set )
5862import Data.Vector qualified as RegularVector
5963import Network.Wreq
6064import Network.Wreq.Session qualified as NetSession
@@ -69,11 +73,24 @@ import Control.Concurrent.MVar (MVar, newMVar, readMVar, modifyMVar_)
6973
7074type Fetcher t m = App m = > Query t -> m (EVM t () )
7175
76+ data FetchStatus = Cached | Fresh
77+ deriving (Show , Eq )
78+
79+ data FetchResult a
80+ = FetchSuccess a FetchStatus
81+ | FetchFailure FetchStatus
82+ | FetchError Text
83+ deriving (Show , Eq )
84+
7285data Session = Session
73- { sess :: NetSession. Session
74- , latestBlockNum :: MVar (Maybe W256 )
75- , sharedCache :: MVar FetchCache
76- , cacheDir :: Maybe FilePath
86+ { sess :: NetSession. Session
87+ , latestBlockNum :: MVar (Maybe W256 )
88+ , sharedCache :: MVar FetchCache
89+ , cacheDir :: Maybe FilePath
90+ -- Track ephemeral failures (network errors, not found, etc.)
91+ -- These are NOT persisted to disk
92+ , failedContracts :: MVar (Set Addr )
93+ , failedSlots :: MVar (Set (Addr , W256 ))
7794 }
7895
7996data FetchCache = FetchCache
@@ -178,39 +195,33 @@ addFetchCache sess address ctrct = do
178195fetchQuery
179196 :: Show a
180197 => BlockNumber
181- -> (Value -> IO (Maybe Value ))
198+ -> (Value -> IO (Either Text Value ))
182199 -> RpcQuery a
183- -> IO (Maybe a )
200+ -> IO (Either Text a )
184201fetchQuery n f q =
185202 case q of
186203 QueryCode addr -> do
187204 m <- f (rpc " eth_getCode" [toRPC addr, toRPC n])
188- pure $ do
189- t <- preview _String <$> m
190- hexText <$> t
205+ pure $ m >>= \ v -> maybeToRight " Parse error" (hexText <$> preview _String v)
191206 QueryNonce addr -> do
192207 m <- f (rpc " eth_getTransactionCount" [toRPC addr, toRPC n])
193- pure $ do
194- t <- preview _String <$> m
195- readText <$> t
208+ pure $ m >>= \ v -> maybeToRight " Parse error" (readText <$> preview _String v)
196209 QueryBlock -> do
197210 m <- f (rpc " eth_getBlockByNumber" [toRPC n, toRPC False ])
198- pure $ m >>= parseBlock
211+ pure $ m >>= \ v -> maybeToRight " Parse error " ( parseBlock v)
199212 QueryBalance addr -> do
200213 m <- f (rpc " eth_getBalance" [toRPC addr, toRPC n])
201- pure $ do
202- t <- preview _String <$> m
203- readText <$> t
214+ pure $ m >>= \ v -> maybeToRight " Parse error" (readText <$> preview _String v)
204215 QuerySlot addr slot -> do
205216 m <- f (rpc " eth_getStorageAt" [toRPC addr, toRPC slot, toRPC n])
206- pure $ do
207- t <- preview _String <$> m
208- readText <$> t
217+ pure $ m >>= \ v -> maybeToRight " Parse error" (readText <$> preview _String v)
209218 QueryChainId -> do
210219 m <- f (rpc " eth_chainId" [toRPC n])
211- pure $ do
212- t <- preview _String <$> m
213- readText <$> t
220+ pure $ m >>= \ v -> maybeToRight " Parse error" (readText <$> preview _String v)
221+
222+ maybeToRight :: b -> Maybe a -> Either b a
223+ maybeToRight _ (Just x) = Right x
224+ maybeToRight y Nothing = Left y
214225
215226parseBlock :: (AsValue s , Show s ) => s -> Maybe Block
216227parseBlock j = do
@@ -265,31 +276,55 @@ instance FromJSON Block where
265276 <*> v .: " maxCodeSize"
266277 <*> pure feeSchedule
267278
268- fetchWithSession :: Text -> NetSession. Session -> Value -> IO (Maybe Value )
279+ fetchWithSession :: Text -> NetSession. Session -> Value -> IO (Either Text Value )
269280fetchWithSession url sess x = do
270281 r <- asValue =<< NetSession. post sess (unpack url) x
271- pure (r ^? (lensVL responseBody) % key " result" )
272-
273- fetchContractWithSession :: Config -> Session -> BlockNumber -> Text -> Addr -> IO (Maybe RPCContract )
282+ let body = r ^. (lensVL responseBody)
283+ case body ^? key " result" of
284+ Just val -> pure $ Right val
285+ Nothing -> case body ^? key " error" of
286+ Just err -> pure $ Left $ pack $ show err
287+ Nothing -> pure $ Left " Unknown RPC error"
288+
289+ fetchContractWithSession :: Config -> Session -> BlockNumber -> Text -> Addr -> IO (FetchResult RPCContract )
274290fetchContractWithSession conf sess nPre url addr = do
275291 n <- getLatestBlockNum conf sess nPre url
292+ -- Check successful cache first
276293 cache <- readMVar sess. sharedCache
277294 case Map. lookup addr cache. contractCache of
278295 Just c -> do
279296 when (conf. debug) $ putStrLn $ " -> Using cached contract at " ++ show addr
280- pure $ Just c
297+ pure ( FetchSuccess c Cached )
281298 Nothing -> do
282- when (conf. debug) $ putStrLn $ " -> Fetching contract at " ++ show addr
283- runMaybeT $ do
284- let fetch :: Show a => RpcQuery a -> IO (Maybe a )
299+ -- Check failure cache
300+ failures <- readMVar sess. failedContracts
301+ if Set. member addr failures
302+ then do
303+ when (conf. debug) $ putStrLn $ " -> Skipping previously failed contract " ++ show addr
304+ pure (FetchFailure Cached )
305+ else do
306+ -- Attempt fetch
307+ when (conf. debug) $ putStrLn $ " -> Fetching contract at " ++ show addr
308+ let fetch :: Show a => RpcQuery a -> IO (Either Text a )
285309 fetch = fetchQuery n (fetchWithSession url sess. sess)
286- code <- MaybeT $ fetch (QueryCode addr)
287- nonce <- MaybeT $ fetch (QueryNonce addr)
288- balance <- MaybeT $ fetch (QueryBalance addr)
289- let contr = RPCContract (ByteStringS code) nonce balance
290- liftIO $ modifyMVar_ sess. sharedCache $ \ c ->
291- pure $ c { contractCache = Map. insert addr contr c. contractCache }
292- pure contr
310+
311+ codeRes <- fetch (QueryCode addr)
312+ nonceRes <- fetch (QueryNonce addr)
313+ balRes <- fetch (QueryBalance addr)
314+
315+ case (codeRes, nonceRes, balRes) of
316+ (Right c, Right no, Right ba) -> do
317+ let contr = RPCContract (ByteStringS c) no ba
318+ if c /= BS. empty
319+ then do
320+ modifyMVar_ sess. sharedCache $ \ x -> pure $ x { contractCache = Map. insert addr contr x. contractCache }
321+ pure (FetchSuccess contr Fresh )
322+ else do
323+ modifyMVar_ sess. failedContracts $ \ f -> pure $ Set. insert addr f
324+ pure (FetchFailure Fresh )
325+ (Left e, _, _) -> pure (FetchError e)
326+ (_, Left e, _) -> pure (FetchError e)
327+ (_, _, Left e) -> pure (FetchError e)
293328
294329-- In case the user asks for Latest, and we have not yet established what Latest is,
295330-- we fetch the block to find out. Otherwise, we update Latest to the value we have stored
@@ -319,23 +354,69 @@ makeContractFromRPC (RPCContract (ByteStringS code) nonce balance) =
319354 & set # external True
320355
321356-- Needed for Echidna only
322- fetchSlotWithCache :: Config -> Session -> BlockNumber -> Text -> Addr -> W256 -> IO (Maybe W256 )
357+ fetchSlotWithCache :: Config -> Session -> BlockNumber -> Text -> Addr -> W256 -> IO (FetchResult W256 )
323358fetchSlotWithCache conf sess nPre url addr slot = do
324359 n <- getLatestBlockNum conf sess nPre url
360+ -- Check successful cache
325361 cache <- readMVar sess. sharedCache
326362 case Map. lookup (addr, slot) cache. slotCache of
327363 Just s -> do
328364 when (conf. debug) $ putStrLn $ " -> Using cached slot value for slot " <> show slot <> " at " <> show addr
329- pure $ Just s
365+ pure ( FetchSuccess s Cached )
330366 Nothing -> do
331- when (conf. debug) $ putStrLn $ " -> Fetching slot " <> show slot <> " at " <> show addr
332- ret <- fetchSlotWithSession sess. sess n url addr slot
333- when (isJust ret) $ let val = fromJust ret in
334- modifyMVar_ sess. sharedCache $ \ c ->
335- pure $ c { slotCache = Map. insert (addr, slot) val c. slotCache }
336- pure ret
337-
338- fetchSlotWithSession :: NetSession. Session -> BlockNumber -> Text -> Addr -> W256 -> IO (Maybe W256 )
367+ -- Check failure cache
368+ failures <- readMVar sess. failedSlots
369+ if Set. member (addr, slot) failures
370+ then do
371+ when (conf. debug) $ putStrLn $ " -> Skipping previously failed slot " <> show slot <> " at " <> show addr
372+ pure (FetchFailure Cached )
373+ else do
374+ -- Attempt fetch
375+ when (conf. debug) $ putStrLn $ " -> Fetching slot " <> show slot <> " at " <> show addr
376+ ret <- fetchSlotWithSession sess. sess n url addr slot
377+ case ret of
378+ Right val -> do
379+ -- Success: cache it
380+ modifyMVar_ sess. sharedCache $ \ c ->
381+ pure $ c { slotCache = Map. insert (addr, slot) val c. slotCache }
382+ pure (FetchSuccess val Fresh )
383+ Left err -> do
384+ pure (FetchError err)
385+
386+ -- | Get the complete cache state including both successes and failures
387+ -- Returns in the format expected by Echidna's UI:
388+ -- - Map Addr (Maybe Contract): Just = success, Nothing = failure
389+ -- - Map Addr (Map W256 (Maybe W256)): Just = success, Nothing = failure
390+ getCacheState
391+ :: Session
392+ -> IO (Map. Map Addr (Maybe Contract ), Map. Map Addr (Map. Map W256 (Maybe W256 )))
393+ getCacheState sess = do
394+ cache <- readMVar sess. sharedCache
395+ failedContracts <- readMVar sess. failedContracts
396+ failedSlots <- readMVar sess. failedSlots
397+
398+ -- Convert contract cache
399+ let successfulContracts = fmap (Just . makeContractFromRPC) cache. contractCache
400+ let allContracts = successfulContracts
401+ <> Map. fromSet (const Nothing ) failedContracts
402+
403+ -- Convert slot cache: group by address
404+ let successfulSlotsByAddr = Map. foldrWithKey
405+ (\ (addr, slot) value acc ->
406+ Map. insertWith Map. union addr (Map. singleton slot (Just value)) acc)
407+ Map. empty
408+ cache. slotCache
409+
410+ -- Add failed slots
411+ let allSlots = Set. foldr
412+ (\ (addr, slot) acc ->
413+ Map. insertWith Map. union addr (Map. singleton slot Nothing ) acc)
414+ successfulSlotsByAddr
415+ failedSlots
416+
417+ pure (allContracts, allSlots)
418+
419+ fetchSlotWithSession :: NetSession. Session -> BlockNumber -> Text -> Addr -> W256 -> IO (Either Text W256 )
339420fetchSlotWithSession sess n url addr slot =
340421 fetchQuery n (fetchWithSession url sess) (QuerySlot addr slot)
341422
@@ -357,12 +438,12 @@ internalBlockFetch conf sess n url = do
357438 when (conf. debug) $ putStrLn $ " Fetching block " ++ show n ++ " from " ++ unpack url
358439 ret <- fetchQuery n (fetchWithSession url sess. sess) QueryBlock
359440 case ret of
360- Nothing -> pure ret
361- Just b -> do
441+ Left _ -> pure Nothing
442+ Right b -> do
362443 let bn = forceLit b. number
363444 liftIO $ modifyMVar_ sess. sharedCache $ \ c ->
364445 pure $ c { blockCache = Map. insert bn b c. blockCache }
365- pure ret
446+ pure ( Just b)
366447
367448cacheFileName :: W256 -> FilePath
368449cacheFileName n = " rpc-cache-" ++ T. unpack (showDec Unsigned n) ++ " .json"
@@ -405,7 +486,10 @@ mkSession cacheDir mblock = do
405486 _ -> pure emptyCache
406487 cache <- liftIO $ newMVar initialCache
407488 latestBlockNum <- liftIO $ newMVar Nothing
408- pure $ Session sess latestBlockNum cache cacheDir
489+ -- Initialize ephemeral failure tracking
490+ failedContracts <- liftIO $ newMVar Set. empty
491+ failedSlots <- liftIO $ newMVar Set. empty
492+ pure $ Session sess latestBlockNum cache cacheDir failedContracts failedSlots
409493
410494mkSessionWithoutCache :: App m => m Session
411495mkSessionWithoutCache = mkSession Nothing Nothing
@@ -460,10 +544,11 @@ oracle solvers preSess rpcInfo q = do
460544 Nothing -> do
461545 when (conf. debug) $ liftIO $ putStrLn $ " Fetching contract at " ++ show addr
462546 let (block, url) = fromJust rpcInfo. blockNumURL
463- contract <- liftIO $ fmap (fmap makeContractFromRPC) $ fetchContractWithSession conf sess block url addr
464- case contract of
465- Just x -> pure $ continue x
466- Nothing -> internalError $ " oracle error: " ++ show q
547+ res <- liftIO $ fetchContractWithSession conf sess block url addr
548+ case res of
549+ FetchSuccess x _ -> pure $ continue (makeContractFromRPC x)
550+ FetchFailure _ -> internalError $ " oracle error: " ++ show q
551+ FetchError e -> internalError $ " oracle error: " ++ show e
467552 where
468553 nothingContract = case base of
469554 AbstractBase -> unknownContract (LitAddr addr)
@@ -485,21 +570,21 @@ oracle solvers preSess rpcInfo q = do
485570 let (block, url) = fromJust rpcInfo. blockNumURL
486571 n <- liftIO $ getLatestBlockNum conf sess block url
487572 ret <- liftIO $ fetchSlotWithSession sess. sess n url addr slot
488- when (isJust ret) $ let val = fromJust ret in
489- liftIO $ modifyMVar_ sess. sharedCache $ \ c ->
490- pure $ c { slotCache = Map. insert (addr, slot) val c. slotCache }
491573 case ret of
492- Just x -> pure $ continue x
493- Nothing -> internalError $ " oracle error: " ++ show q
574+ Right val -> do
575+ liftIO $ modifyMVar_ sess. sharedCache $ \ c ->
576+ pure $ c { slotCache = Map. insert (addr, slot) val c. slotCache }
577+ pure $ continue val
578+ Left err -> internalError $ " oracle error: " ++ show err
494579
495580 PleaseReadEnv variable continue -> do
496581 value <- liftIO $ lookupEnv variable
497582 pure . continue $ fromMaybe " " value
498583
499584 where
500585 -- special values such as 0, 0xdeadbeef, 0xacab, hevm cheatcodes, and the precompile addresses
501- isAddressSpecial addr = addr <= 0xdeadbeef || addr == 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D
502-
586+ isAddressSpecial addr = addr <= 0xdeadbeef || addr == 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D
587+
503588
504589getSolutions :: forall m . App m => SolverGroup -> Expr EWord -> Int -> Prop -> m (Maybe [W256 ])
505590getSolutions solvers symExprPreSimp numBytes pathconditions = do
0 commit comments