@@ -11,7 +11,7 @@ import Control.Monad.ST (ST, stToIO, RealWorld)
1111import Control.Monad.State.Strict (MonadState (get , put ), execState , runStateT , MonadIO (liftIO ), gets , modify' , execStateT )
1212import Data.Bits
1313import Data.ByteString qualified as BS
14- import Data.IORef (readIORef , newIORef , writeIORef , modifyIORef' )
14+ import Data.IORef (IORef , readIORef , newIORef , writeIORef , modifyIORef' )
1515import Data.Map qualified as Map
1616import Data.Maybe (fromMaybe , fromJust )
1717import 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+
207205logMsg :: (MonadIO m , MonadReader Env m ) => String -> m ()
208206logMsg 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
316321initialVM :: Bool -> ST RealWorld (VM Concrete )
317322initialVM ffi = do
0 commit comments