Skip to content

Commit 67820b2

Browse files
simplified exploration code and added shrinking
1 parent 69fc746 commit 67820b2

File tree

4 files changed

+121
-32
lines changed

4 files changed

+121
-32
lines changed

lib/Echidna/Campaign.hs

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Echidna.Campaign where
55

66
import Control.Concurrent
77
import Control.DeepSeq (force)
8-
import Control.Monad (replicateM, replicateM_, when, unless, void, forM_)
8+
import Control.Monad (replicateM, when, unless, void, forM_)
99
import Control.Monad.Catch (MonadThrow(..))
1010
import Control.Monad.Random.Strict (MonadRandom, RandT, evalRandT)
1111
import Control.Monad.Reader (MonadReader, asks, liftIO, ask)
@@ -42,7 +42,7 @@ import Echidna.Shrink (shrinkTest)
4242
import Echidna.Solidity (chooseContract)
4343
import Echidna.SymExec.Common (extractTxs, extractErrors)
4444
import Echidna.SymExec.Symbolic (forceAddr)
45-
import Echidna.SymExec.Exploration (exploreContract)
45+
import Echidna.SymExec.Exploration (exploreContract, getTargetMethodFromTx, getRandomTargetMethod)
4646
import Echidna.SymExec.Verification (verifyMethod)
4747
import Echidna.Test
4848
import Echidna.Transaction
@@ -51,7 +51,7 @@ import Echidna.Types.Campaign
5151
import Echidna.Types.Corpus (Corpus, corpusSize)
5252
import Echidna.Types.Coverage (coverageStats)
5353
import Echidna.Types.Config
54-
import Echidna.Types.Random (rElem, shuffleIO)
54+
import Echidna.Types.Random (rElem)
5555
import Echidna.Types.Signature (FunctionName)
5656
import Echidna.Types.Test
5757
import Echidna.Types.Test qualified as Test
@@ -116,8 +116,7 @@ runSymWorker
116116
-- ^ Initial corpus of transactions
117117
-> Maybe Text -- ^ Specified contract name
118118
-> m (WorkerStopReason, WorkerState)
119-
runSymWorker callback vm dict workerId initialCorpus name = do
120-
shuffleCorpus <- liftIO $ shuffleIO initialCorpus
119+
runSymWorker callback vm dict workerId _ name = do
121120
cfg <- asks (.cfg)
122121
let nworkers = getNFuzzWorkers cfg.campaignConf -- getNFuzzWorkers, NOT getNWorkers
123122
eventQueue <- asks (.eventQueue)
@@ -134,13 +133,6 @@ runSymWorker callback vm dict workerId initialCorpus name = do
134133
flip evalRandT (mkStdGen effectiveSeed) $ do -- unused but needed for callseq
135134
lift callback
136135
listenerLoop listenerFunc chan nworkers
137-
void $ replayCorpus vm initialCorpus
138-
if null shuffleCorpus then
139-
replicateM_ 10 $ symexecTxs True [] -- TODO: determine how many times to symexec here
140-
else
141-
mapM_ (symexecTxs False . snd) shuffleCorpus
142-
liftIO $ putStrLn "Symbolic exploration started purely random!"
143-
replicateM_ 100 $ mapM_ (symexecTxs True . snd) shuffleCorpus
144136
pure SymbolicExplorationDone
145137

146138
where
@@ -164,8 +156,53 @@ runSymWorker callback vm dict workerId initialCorpus name = do
164156
listenerFunc (_, WorkerEvent _ _ (NewCoverage {transactions})) = do
165157
void $ callseq vm transactions
166158
symexecTxs False transactions
159+
shrinkAndRandomlyExplore transactions (10 :: Int)
167160
listenerFunc _ = pure ()
168161

162+
shrinkAndRandomlyExplore _ 0 = do
163+
testRefs <- asks (.testRefs)
164+
tests <- liftIO $ traverse readIORef testRefs
165+
CampaignConf{shrinkLimit} <- asks (.cfg.campaignConf)
166+
if any shrinkable tests then shrinkLoop shrinkLimit else return ()
167+
168+
shrinkAndRandomlyExplore txs n = do
169+
testRefs <- asks (.testRefs)
170+
tests <- liftIO $ traverse readIORef testRefs
171+
CampaignConf{stopOnFail, shrinkLimit} <- asks (.cfg.campaignConf)
172+
if stopOnFail && any final tests then
173+
lift callback -- >> pure FastFailed
174+
else if any shrinkable tests then do
175+
shrinkLoop shrinkLimit
176+
shrinkAndRandomlyExplore txs n
177+
else do
178+
symexecTxs False txs
179+
shrinkAndRandomlyExplore txs (n - 1)
180+
181+
182+
shrinkable test =
183+
case test.state of
184+
-- we shrink only tests which were solved on this
185+
-- worker, see 'updateOpenTest'
186+
Large _ | test.workerId == Just workerId -> True
187+
_ -> False
188+
189+
final test =
190+
case test.state of
191+
Solved -> True
192+
Failed _ -> True
193+
_ -> False
194+
195+
196+
shrinkLoop 0 = return ()
197+
shrinkLoop n = do
198+
lift callback
199+
updateTests $ \test -> do
200+
if test.workerId == Just workerId then
201+
shrinkTest vm test
202+
else
203+
pure Nothing
204+
shrinkLoop (n - 1)
205+
169206
symexecTxs onlyRandom txs = mapM_ symexecTx =<< txsToTxAndVmsSym onlyRandom txs
170207

171208
-- | Turn a list of transactions into inputs for symexecTx:
@@ -198,8 +235,21 @@ runSymWorker callback vm dict workerId initialCorpus name = do
198235
dapp <- asks (.dapp)
199236
let cs = Map.elems dapp.solcByName
200237
contract <- chooseContract cs name
201-
(threadId, symTxsChan) <- exploreContract contract tx vm'
202-
238+
failedTests <- findFailedTests
239+
let failedTestSignatures = map getAssertionSignature failedTests
240+
case tx of
241+
Nothing -> getRandomTargetMethod contract failedTestSignatures >>= \case
242+
Nothing -> do
243+
error "No suitable method found for symbolic execution"
244+
Just method -> exploreAndVerify contract method vm' txsBase
245+
Just t -> getTargetMethodFromTx t contract failedTestSignatures >>= \case
246+
Nothing -> do
247+
return ()
248+
Just method -> do
249+
exploreAndVerify contract method vm' txsBase
250+
251+
exploreAndVerify contract method vm' txsBase = do
252+
(threadId, symTxsChan) <- exploreContract contract method vm'
203253
modify' (\ws -> ws { runningThreads = [threadId] })
204254
lift callback
205255

@@ -217,10 +267,8 @@ runSymWorker callback vm dict workerId initialCorpus name = do
217267
-- We can't do callseq vm' [symTx] because callseq might post the full call sequence as an event
218268
newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm (txsBase <> [symTx])) txs
219269

220-
when (not newCoverage && null errors && not (null txs)) ( do
221-
liftIO $ mapM_ print txsBase
222-
liftIO $ putStrLn $ "Last txs: " <> show txs
223-
error "No errors but symbolic execution found valid txs breaking assertions. Something is wrong.")
270+
when (not newCoverage && null errors && not (null txs)) (
271+
pushWorkerEvent $ SymExecError "No errors but symbolic execution found valid txs breaking assertions. Something is wrong.")
224272
unless newCoverage (pushWorkerEvent SymNoNewCoverage)
225273

226274
verifyMethods = do
@@ -574,6 +622,14 @@ updateTests f = do
574622
Just test' -> liftIO $ writeIORef testRef test'
575623
Nothing -> pure ()
576624

625+
findFailedTests
626+
:: (MonadIO m, MonadReader Env m, MonadState WorkerState m)
627+
=> m [EchidnaTest]
628+
findFailedTests = do
629+
testRefs <- asks (.testRefs)
630+
tests <- liftIO $ traverse readIORef testRefs
631+
pure $ filter didFail tests
632+
577633
-- | Update an open test after checking if it is falsified by the 'reproducer'
578634
updateOpenTest
579635
:: (MonadIO m, MonadThrow m, MonadRandom m, MonadReader Env m, MonadState WorkerState m)

lib/Echidna/SymExec/Exploration.hs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ module Echidna.SymExec.Exploration where
55
import Control.Applicative ((<|>))
66
import Control.Concurrent (ThreadId, forkIO)
77
import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)
8-
import Control.Monad (forM)
98
import Control.Monad.Catch (MonadThrow(..))
109
import Control.Monad.IO.Class (MonadIO)
1110
import Control.Monad.Reader (MonadReader, ask, asks, runReaderT, liftIO)
1211
import Data.Map qualified as Map
1312
import Data.Maybe (fromJust)
1413
import Data.Set qualified as Set
15-
import Data.Text (Text)
14+
import Data.Text (unpack, Text)
1615
import Data.Text.Encoding (encodeUtf8)
16+
import Data.List.NonEmpty (fromList)
1717
import EVM.Effects (defaultEnv, defaultConfig, Config(..), Env(..))
1818
import EVM.Solidity (SolcContract(..), Method(..))
1919
import EVM.Solvers (withSolvers)
@@ -24,10 +24,10 @@ import Control.Monad.ST (RealWorld)
2424

2525
import Echidna.Types.Campaign (CampaignConf(..))
2626
import Echidna.Types.Config (Env(..), EConfig(..), OperationMode(..), OutputFormat(..), UIConf(..))
27-
import Echidna.Types.Random (shuffleIO)
2827
import Echidna.Types.World (World(..))
2928
import Echidna.Types.Solidity (SolConf(..))
3029
import Echidna.Types.Tx (Tx(..), TxCall(..))
30+
import Echidna.Types.Random (rElem)
3131
import Echidna.SymExec.Common (suitableForSymExec, exploreMethod, rpcFetcher, TxOrError(..), PartialsLogs)
3232

3333
-- | Uses symbolic execution to find transactions which would increase coverage.
@@ -39,6 +39,40 @@ import Echidna.SymExec.Common (suitableForSymExec, exploreMethod, rpcFetcher, Tx
3939
-- symbolic execution.
4040
-- The Tx argument, if present, must have a .call value of type SolCall.
4141

42+
getTargetMethodFromTx :: (MonadIO m, MonadReader Echidna.Types.Config.Env m) => Tx -> SolcContract -> [String] -> m (Maybe Method)
43+
getTargetMethodFromTx (Tx { call = SolCall (methodName, _) }) contract failedProperties = do
44+
env <- ask
45+
let allMethods = Map.assocs contract.abiMap
46+
assertSigs = env.world.assertSigs
47+
(selector, method) = case filter (\(_, m) -> m.name == methodName) allMethods of
48+
[] -> error $ "Method " ++ show methodName ++ " not found in contract ABI"
49+
(x:_) -> x
50+
51+
if (null assertSigs || selector `elem` assertSigs) && suitableForSymExec method && (unpack $ method.methodSignature) `notElem` failedProperties
52+
then return $ Just method
53+
else return Nothing
54+
55+
getTargetMethodFromTx _ _ _ = return Nothing
56+
57+
-- This function selects a random method from the contract's ABI to explore.
58+
-- It uses the campaign configuration to determine which methods are suitable for symbolic execution.
59+
-- Additionally, it filter methods that are associated with failed properties, if any.
60+
getRandomTargetMethod :: (MonadIO m, MonadReader Echidna.Types.Config.Env m) => SolcContract -> [String] -> m (Maybe Method)
61+
getRandomTargetMethod contract failedProperties = do
62+
env <- ask
63+
let allMethods = Map.assocs contract.abiMap
64+
assertSigs = env.world.assertSigs
65+
--(selector, method) = case filter (\(_, m) -> m.name == method.name) allMethods of
66+
-- [] -> error $ "Method " ++ show methodName ++ " not found in contract ABI"
67+
-- (x:_) -> x
68+
filterFunc (selector, method) = (null assertSigs || selector `elem` assertSigs) && suitableForSymExec method && (unpack $ method.methodSignature) `notElem` failedProperties
69+
filteredMethods = filter filterFunc allMethods
70+
71+
case filteredMethods of
72+
[] -> return Nothing
73+
_ -> liftIO $ rElem (fromList $ map (Just . snd) filteredMethods)
74+
75+
4276
-- | Filters methods based on the campaign configuration.
4377
-- If symExecTargets is Just, it filters methods by their name.
4478
-- If symExecTargets is Nothing, it filters methods by their signature and checks if they are suitable for symbolic execution.
@@ -51,22 +85,18 @@ filterTarget symExecTargets assertSigs tx method =
5185
_ -> (null assertSigs || methodSig `elem` assertSigs) && suitableForSymExec method
5286
where methodSig = abiKeccak $ encodeUtf8 method.methodSignature
5387

54-
exploreContract :: (MonadIO m, MonadThrow m, MonadReader Echidna.Types.Config.Env m) => SolcContract -> Maybe Tx -> EVM.Types.VM Concrete RealWorld -> m (ThreadId, MVar ([TxOrError], PartialsLogs))
55-
exploreContract contract tx vm = do
88+
exploreContract :: (MonadIO m, MonadThrow m, MonadReader Echidna.Types.Config.Env m) => SolcContract -> Method -> EVM.Types.VM Concrete RealWorld -> m (ThreadId, MVar ([TxOrError], PartialsLogs))
89+
exploreContract contract method vm = do
5690
conf <- asks (.cfg)
57-
env <- ask
5891
contractCacheRef <- asks (.fetchContractCache)
5992
slotCacheRef <- asks (.fetchSlotCache)
6093
let
61-
allMethods = Map.elems contract.abiMap
62-
filteredMethods = filter (filterTarget conf.campaignConf.symExecTargets env.world.assertSigs tx) allMethods
63-
methods = filteredMethods
6494
timeoutSMT = Just (fromIntegral conf.campaignConf.symExecTimeout)
6595
maxIters = Just conf.campaignConf.symExecMaxIters
6696
maxExplore = Just (fromIntegral conf.campaignConf.symExecMaxExplore)
6797
askSmtIters = conf.campaignConf.symExecAskSMTIters
6898
rpcInfo = rpcFetcher conf.rpcUrl (fromIntegral <$> conf.rpcBlock)
69-
defaultSender = fromJust $ fmap (.dst) tx <|> Set.lookupMin conf.solConf.sender <|> Just 0
99+
defaultSender = fromJust $ Set.lookupMin conf.solConf.sender <|> Just 0
70100

71101
threadIdChan <- liftIO newEmptyMVar
72102
doneChan <- liftIO newEmptyMVar
@@ -78,12 +108,11 @@ exploreContract contract tx vm = do
78108

79109
liftIO $ flip runReaderT runtimeEnv $ withSolvers conf.campaignConf.symExecSMTSolver (fromIntegral conf.campaignConf.symExecNSolvers) 1 timeoutSMT $ \solvers -> do
80110
threadId <- liftIO $ forkIO $ flip runReaderT runtimeEnv $ do
81-
shuffleMethods <- liftIO $ shuffleIO methods
82111
-- For now, we will be exploring a single method at a time.
83112
-- In some cases, this methods list will have only one method, but in other cases, it will have several methods.
84113
-- This is to improve the user experience, as it will produce results more often, instead having to wait for exploring several
85-
res <- forM (take 1 shuffleMethods) $ \method -> exploreMethod method contract vm defaultSender conf veriOpts solvers rpcInfo contractCacheRef slotCacheRef
86-
liftIO $ putMVar resultChan (concatMap fst res, concatMap snd res)
114+
res <- exploreMethod method contract vm defaultSender conf veriOpts solvers rpcInfo contractCacheRef slotCacheRef
115+
liftIO $ putMVar resultChan res
87116
--liftIO $ print "done"
88117
liftIO $ putMVar doneChan ()
89118
liftIO $ putMVar threadIdChan threadId

lib/Echidna/Types/Test.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ getAssertionSignature :: EchidnaTest -> String
130130
getAssertionSignature EchidnaTest{testType = AssertionTest _ sig _} = unpack $ encodeSig sig
131131
getAssertionSignature _ = error "Not an assertion test"
132132

133+
getAssertionFunctionName :: EchidnaTest -> String
134+
getAssertionFunctionName EchidnaTest{testType = AssertionTest _ (name, _) _} = unpack $ name
135+
getAssertionFunctionName _ = error "Not an assertion test"
136+
133137
isOpen :: EchidnaTest -> Bool
134138
isOpen t = case t.state of
135139
Open -> True

stack.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ packages:
66

77
extra-deps:
88
- git: https://github.com/ethereum/hevm.git
9-
commit: d282dff6e0d9ea9f7cf02e17e8ac0b268ef634da
9+
commit: 2931f09fcbbca68911421fbe2f2f21ebebdb5332
1010

1111
- smt2-parser-0.1.0.1@sha256:1e1a4565915ed851c13d1e6b8bb5185cf5d454da3b43170825d53e221f753d77,1421
1212
- spawn-0.3@sha256:b91e01d8f2b076841410ae284b32046f91471943dc799c1af77d666c72101f02,1162

0 commit comments

Comments
 (0)