Skip to content

Commit 183e142

Browse files
samalws-tobgustavo-grieco
authored andcommitted
Simplify Exec.hs
1 parent c010ea5 commit 183e142

File tree

1 file changed

+136
-131
lines changed

1 file changed

+136
-131
lines changed

lib/Echidna/Exec.hs

Lines changed: 136 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Control.Monad.ST (ST, stToIO, RealWorld)
1111
import Control.Monad.State.Strict (MonadState(get, put), execState, runStateT, MonadIO(liftIO), gets, modify', execStateT)
1212
import Data.Bits
1313
import Data.ByteString qualified as BS
14-
import Data.IORef (readIORef, newIORef, writeIORef, modifyIORef')
14+
import Data.IORef (IORef, readIORef, newIORef, writeIORef, modifyIORef')
1515
import Data.Map qualified as Map
1616
import Data.Maybe (fromMaybe, fromJust)
1717
import Data.Text qualified as T
@@ -103,75 +103,8 @@ execTxWith executeTx tx = do
103103
pure vmResult
104104
where
105105
runFully = do
106-
config <- asks (.cfg)
107-
-- TODO: Is the latest block a good default? It makes fuzzing hard to reproduce. Rethink this.
108-
let rpcBlock = maybe EVM.Fetch.Latest (EVM.Fetch.BlockNumber . fromIntegral) config.rpcBlock
109-
110106
vmResult <- executeTx
111-
-- For queries, we halt execution because the VM needs some additional
112-
-- information from the outside. We provide this information and resume
113-
-- the execution by recursively calling `runFully`.
114-
case getQuery vmResult of
115-
-- A previously unknown contract is required
116-
Just q@(PleaseFetchContract addr _ continuation) -> do
117-
--logMsg $ "INFO: Performing RPC: " <> show q
118-
case config.rpcUrl of
119-
Just rpcUrl -> do
120-
session <- asks (.fetchSession)
121-
ret <- liftIO $ safeFetchContractFrom session rpcBlock rpcUrl addr
122-
case ret of
123-
EVM.Fetch.FetchSuccess contract _ -> do
124-
fromEVM (continuation contract)
125-
EVM.Fetch.FetchFailure _ -> do
126-
fromEVM (continuation emptyAccount)
127-
EVM.Fetch.FetchError e -> do
128-
error $ "ERROR: Failed to fetch contract: " <> show q <> " " <> T.unpack e
129-
Nothing -> do
130-
--logMsg $ "ERROR: Requested RPC but it is not configured: " <> show q
131-
-- TODO: How should we fail here? RPC is not configured but VM
132-
-- wants to fetch
133-
fromEVM (continuation emptyAccount)
134-
runFully -- resume execution
135-
136-
-- A previously unknown slot is required
137-
Just q@(PleaseFetchSlot addr slot continuation) -> do
138-
case config.rpcUrl of
139-
Just rpcUrl -> do
140-
session <- asks (.fetchSession)
141-
ret <- liftIO $ safeFetchSlotFrom session rpcBlock rpcUrl addr slot
142-
case ret of
143-
EVM.Fetch.FetchSuccess value status -> do
144-
-- Log only in text mode, ignoring quiet flag as this is important info
145-
when (status == EVM.Fetch.Fresh) $ logMsg $ "Fetched new slot: " <> show q
146-
fromEVM (continuation value)
147-
EVM.Fetch.FetchFailure _ -> do
148-
fromEVM (continuation 0)
149-
EVM.Fetch.FetchError e -> do
150-
error $ "ERROR: Failed to fetch slot: " <> show q <> " " <> T.unpack e
151-
Nothing -> do
152-
--logMsg $ "ERROR: Requested RPC but it is not configured: " <> show q
153-
-- Use the zero slot
154-
fromEVM (continuation 0)
155-
runFully -- resume execution
156-
157-
-- Execute a FFI call
158-
Just (PleaseDoFFI (cmd : args) envs continuation) -> do
159-
existingEnv <- liftIO getEnvironment
160-
let mergedEnv = Map.toList $ Map.union envs $ Map.fromList existingEnv
161-
let process = (P.proc cmd args) { P.env = Just mergedEnv }
162-
(_, stdout, _) <- liftIO $ P.readCreateProcessWithExitCode process ""
163-
let encodedResponse = encodeAbiValue $
164-
AbiTuple (V.fromList [AbiBytesDynamic . hexText . T.strip . T.pack $ stdout])
165-
fromEVM (continuation encodedResponse)
166-
runFully
167-
168-
Just (PleaseReadEnv var continuation) -> do
169-
value <- liftIO $ lookupEnv var
170-
fromEVM (continuation $ fromMaybe "" value)
171-
runFully -- resume execution
172-
173-
-- No queries to answer, the tx is fully executed and the result is final
174-
_ -> pure vmResult
107+
maybe (pure vmResult) (\q -> handleQuery q >> runFully) (getQuery vmResult)
175108

176109
-- | Handles reverts, failures and contract creations that might be the result
177110
-- (`vmResult`) of executing transaction `tx`.
@@ -204,6 +137,71 @@ execTxWith executeTx tx = do
204137
modify' $ execState $ loadContract (LitAddr tx.dst)
205138
_ -> pure ()
206139

140+
getRpcInfo = do
141+
config <- asks (.cfg)
142+
-- TODO: Is the latest block a good default? It makes fuzzing hard to reproduce. Rethink this.
143+
let rpcBlock = maybe EVM.Fetch.Latest (EVM.Fetch.BlockNumber . fromIntegral) config.rpcBlock
144+
return (config.rpcUrl, rpcBlock)
145+
146+
-- For queries, we halt execution because the VM needs some additional
147+
-- information from the outside. We provide this information, and then
148+
-- the execution is resumed.
149+
150+
-- A previously unknown contract is required
151+
handleQuery q@(PleaseFetchContract addr _ continuation) = do
152+
(maybeRpcUrl, rpcBlock) <- getRpcInfo
153+
case maybeRpcUrl of
154+
Just rpcUrl -> do
155+
session <- asks (.fetchSession)
156+
ret <- liftIO $ safeFetchContractFrom session rpcBlock rpcUrl addr
157+
case ret of
158+
EVM.Fetch.FetchSuccess contract _ -> do
159+
fromEVM (continuation contract)
160+
EVM.Fetch.FetchFailure _ -> do
161+
fromEVM (continuation emptyAccount)
162+
EVM.Fetch.FetchError e -> do
163+
error $ "ERROR: Failed to fetch contract: " <> show q <> " " <> T.unpack e
164+
Nothing -> do
165+
-- TODO: How should we fail here? RPC is not configured but VM
166+
-- wants to fetch
167+
fromEVM (continuation emptyAccount)
168+
169+
-- A previously unknown slot is required
170+
handleQuery q@(PleaseFetchSlot addr slot continuation) = do
171+
(maybeRpcUrl, rpcBlock) <- getRpcInfo
172+
case maybeRpcUrl of
173+
Just rpcUrl -> do
174+
session <- asks (.fetchSession)
175+
ret <- liftIO $ safeFetchSlotFrom session rpcBlock rpcUrl addr slot
176+
case ret of
177+
EVM.Fetch.FetchSuccess value status -> do
178+
-- Log only in text mode, ignoring quiet flag as this is important info
179+
when (status == EVM.Fetch.Fresh) $ logMsg $ "Fetched new slot: " <> show q
180+
fromEVM (continuation value)
181+
EVM.Fetch.FetchFailure _ -> do
182+
fromEVM (continuation 0)
183+
EVM.Fetch.FetchError e -> do
184+
error $ "ERROR: Failed to fetch slot: " <> show q <> " " <> T.unpack e
185+
Nothing -> do
186+
-- Use the zero slot
187+
fromEVM (continuation 0)
188+
189+
-- Execute a FFI call
190+
handleQuery (PleaseDoFFI (cmd : args) envs continuation) = do
191+
existingEnv <- liftIO getEnvironment
192+
let mergedEnv = Map.toList $ Map.union envs $ Map.fromList existingEnv
193+
let process = (P.proc cmd args) { P.env = Just mergedEnv }
194+
(_, stdout, _) <- liftIO $ P.readCreateProcessWithExitCode process ""
195+
let encodedResponse = encodeAbiValue $
196+
AbiTuple (V.fromList [AbiBytesDynamic . hexText . T.strip . T.pack $ stdout])
197+
fromEVM (continuation encodedResponse)
198+
199+
handleQuery (PleaseDoFFI [] _ _) = error "Malformed FFI call"
200+
201+
handleQuery (PleaseReadEnv var continuation) = do
202+
value <- liftIO $ lookupEnv var
203+
fromEVM (continuation $ fromMaybe "" value)
204+
207205
logMsg :: (MonadIO m, MonadReader Env m) => String -> m ()
208206
logMsg msg = do
209207
cfg <- asks (.cfg)
@@ -249,69 +247,76 @@ execTxWithCov tx = do
249247
_ -> pure False
250248

251249
pure (r, grew || grew')
250+
251+
-- | The same as EVM.exec but collects coverage, will stop on a query
252+
execCov
253+
:: (MonadIO m, MonadState (VM Concrete) m, MonadThrow m)
254+
=> Env
255+
-> IORef CoverageContext
256+
-> m (VMResult Concrete)
257+
execCov env covContextRef = do
258+
vm <- get
259+
(r, vm') <- liftIO $ loop vm
260+
put vm'
261+
pure r
252262
where
253-
-- the same as EVM.exec but collects coverage, will stop on a query
254-
execCov env covContextRef = do
255-
vm <- get
256-
(r, vm') <- liftIO $ loop vm
257-
put vm'
258-
pure r
259-
where
260-
-- | Repeatedly exec a step and add coverage until we have an end result
261-
loop :: VM Concrete -> IO (VMResult Concrete, VM Concrete)
262-
loop !vm = case vm.result of
263-
Nothing -> do
264-
addCoverage vm
265-
stepVM vm >>= loop
266-
Just r -> pure (r, vm)
267-
268-
-- | Execute one instruction on the EVM
269-
stepVM :: VM Concrete -> IO (VM Concrete)
270-
stepVM = stToIO . execStateT (exec1 defaultConfig)
271-
272-
-- | Add current location to the CoverageMap
273-
addCoverage :: VM Concrete -> IO ()
274-
addCoverage !vm = do
275-
let (pc, opIx, depth) = currentCovLoc vm
276-
contract = currentContract vm
277-
covRef = case contract.code of
278-
InitCode _ _ -> env.coverageRefInit
279-
_ -> env.coverageRefRuntime
280-
281-
maybeCovVec <- lookupUsingCodehashOrInsert env.codehashMap contract env.dapp covRef $ do
282-
let
283-
size = case contract.code of
284-
InitCode b _ -> BS.length b
285-
_ -> BS.length . forceBuf . fromJust . view bytecode $ contract
286-
if size == 0 then pure Nothing else do
287-
-- IO for making a new vec
288-
vec <- VMut.new size
289-
-- We use -1 for opIx to indicate that the location was not covered
290-
forM_ [0..size-1] $ \i -> VMut.write vec i (-1, 0, 0)
291-
pure $ Just vec
292-
293-
case maybeCovVec of
294-
Nothing -> pure ()
295-
Just vec -> do
296-
-- TODO: no-op when pc is out-of-bounds. This shouldn't happen but
297-
-- we observed this in some real-world scenarios. This is likely a
298-
-- bug in another place, investigate.
299-
-- ... this should be fixed now, since we use `codeContract` instead
300-
-- of `contract` for everything; it may be safe to remove this check.
301-
when (pc < VMut.length vec) $
302-
VMut.read vec pc >>= \case
303-
(_, depths, results) | depth < 64 && not (depths `testBit` depth) -> do
304-
VMut.write vec pc (opIx, depths `setBit` depth, results `setBit` fromEnum Stop)
305-
writeIORef covContextRef (True, Just (vec, pc))
306-
_ ->
307-
modifyIORef' covContextRef $ \(new, _) -> (new, Just (vec, pc))
308-
309-
-- | Get the VM's current execution location
310-
currentCovLoc vm = (vm.state.pc, fromMaybe 0 $ vmOpIx vm, length vm.frames)
311-
312-
-- | Get the current contract being executed
313-
currentContract vm = fromMaybe (error "no contract information on coverage") $
314-
vm ^? #env % #contracts % at vm.state.codeContract % _Just
263+
-- | Repeatedly exec a step and add coverage until we have an end result
264+
loop :: VM Concrete -> IO (VMResult Concrete, VM Concrete)
265+
loop !vm = case vm.result of
266+
Nothing -> do
267+
addCoverage vm
268+
stepVM vm >>= loop
269+
Just r -> pure (r, vm)
270+
271+
-- | Execute one instruction on the EVM
272+
stepVM :: VM Concrete -> IO (VM Concrete)
273+
stepVM = stToIO . execStateT (exec1 defaultConfig)
274+
275+
-- | Add current location to the CoverageMap
276+
addCoverage :: VM Concrete -> IO ()
277+
addCoverage !vm = do
278+
let (pc, opIx, depth) = currentCovLoc vm
279+
contract = currentContract vm
280+
covRef = case contract.code of
281+
InitCode _ _ -> env.coverageRefInit
282+
_ -> env.coverageRefRuntime
283+
284+
maybeCovVec <- lookupUsingCodehashOrInsert env.codehashMap contract env.dapp covRef $ createCoverageVec contract
285+
286+
case maybeCovVec of
287+
Nothing -> pure ()
288+
Just vec ->
289+
-- TODO: no-op when pc is out-of-bounds. This shouldn't happen but
290+
-- we observed this in some real-world scenarios. This is likely a
291+
-- bug in another place, investigate.
292+
-- ... this should be fixed now, since we use `codeContract` instead
293+
-- of `contract` for everything; it may be safe to remove this check.
294+
when (pc < VMut.length vec) $
295+
VMut.read vec pc >>= \case
296+
(_, depths, results) | depth < 64 && not (depths `testBit` depth) -> do
297+
VMut.write vec pc (opIx, depths `setBit` depth, results `setBit` fromEnum Stop)
298+
writeIORef covContextRef (True, Just (vec, pc))
299+
_ ->
300+
modifyIORef' covContextRef $ \(new, _) -> (new, Just (vec, pc))
301+
302+
createCoverageVec contract = do
303+
let
304+
size = case contract.code of
305+
InitCode b _ -> BS.length b
306+
_ -> BS.length . forceBuf . fromJust . view bytecode $ contract
307+
if size == 0 then pure Nothing else do
308+
-- IO for making a new vec
309+
vec <- VMut.new size
310+
-- We use -1 for opIx to indicate that the location was not covered
311+
forM_ [0..size-1] $ \i -> VMut.write vec i (-1, 0, 0)
312+
pure $ Just vec
313+
314+
-- | Get the VM's current execution location
315+
currentCovLoc vm = (vm.state.pc, fromMaybe 0 $ vmOpIx vm, length vm.frames)
316+
317+
-- | Get the current contract being executed
318+
currentContract vm = fromMaybe (error "no contract information on coverage") $
319+
vm ^? #env % #contracts % at vm.state.codeContract % _Just
315320

316321
initialVM :: Bool -> ST RealWorld (VM Concrete)
317322
initialVM ffi = do

0 commit comments

Comments
 (0)