Skip to content

Commit

Permalink
Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
arcz committed Mar 30, 2023
1 parent 9e5bade commit d198906
Show file tree
Hide file tree
Showing 14 changed files with 64 additions and 43 deletions.
4 changes: 1 addition & 3 deletions lib/Echidna/Campaign.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ instance MonadCatch m => MonadCatch (RandT g m) where

-- | Given a 'Campaign', checks if we can attempt any solves or shrinks without exceeding
-- the limits defined in our 'CampaignConf'.
isDone :: MonadReader Env m => Campaign -> m Bool
isDone :: MonadReader Env m => GenericCampaign a -> m Bool
isDone c | null c.tests = do
conf <- asks (.cfg.campaignConf)
pure $ c.ncallseqs * conf.seqLen >= conf.testLimit
Expand Down Expand Up @@ -143,8 +143,6 @@ execTxOptC :: (MonadIO m, MonadReader Env m, MonadState (VM, Campaign) m, MonadT
execTxOptC tx = do
(vm, camp@Campaign{coverage = oldCov}) <- get
((res, (cov', grew)), vm') <- runStateT (execTxWithCov tx oldCov) vm
-- Update the global coverage map with the one from this tx run
-- let newCov = Map.unionWith Set.union oldCov txCov'
put (vm', camp { coverage = cov' })
when grew $ do
let dict' = case tx.call of
Expand Down
11 changes: 6 additions & 5 deletions lib/Echidna/Exec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Echidna.Types (ExecException(..), Gas, fromEVM)
import Echidna.Types.Buffer (forceBuf)
import Echidna.Types.Coverage (CoverageMap)
import Echidna.Types.Signature (MetadataCache, getBytecodeMetadata, lookupBytecodeMetadata)
import Echidna.Types.Tx (TxCall(..), Tx, TxResult(..), call, dst, initialTimestamp, initialBlockNumber, getResult)
import Echidna.Types.Tx (TxCall(..), Tx, TxResult(..), call, dst, initialTimestamp, initialBlockNumber)
import Echidna.Types.Config (Env(..), EConfig(..), UIConf(..), OperationMode(..), OutputFormat(Text))
import Echidna.Types.Solidity (SolConf(..))
import Echidna.Utility (timePrefix)
Expand Down Expand Up @@ -256,19 +256,20 @@ execTxWithCov tx cov = do
put vm'

-- Update the tx coverage map with the proper result according to the vm result
let meta = currentMeta cache vm'
{-let meta = currentMeta cache vm'
grew' <- liftIO $ case Map.lookup meta cm of
Nothing -> pure False -- shouldnt happen
Just vec -> do
let txResultBit = fromEnum $ getResult $ fst r
let pc = vm'._state._pc
V.read vec pc >>= \case
pure False
V.read vec pc >>= \case
(opIx, depths, txResults) | not (txResults `testBit` txResultBit) -> do
V.write vec pc (opIx, depths, txResults `setBit` txResultBit)
pure True
_ -> pure False
_ -> pure False-}

pure (r, (cm, grew || grew'))
pure (r, (cm, grew))
where
-- the same as EVM.exec but collects coverage, will stop on a query
execCov cache = do
Expand Down
22 changes: 12 additions & 10 deletions lib/Echidna/Output/JSON.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ module Echidna.Output.JSON where
import Data.Aeson hiding (Error)
import Data.ByteString.Base16 qualified as BS16
import Data.ByteString.Lazy (ByteString)
import Data.Foldable qualified as DF
import Data.Map
import Data.Text
import Data.Text.Encoding (decodeUtf8)
import Data.Vector.Unboxed qualified as VU
import Numeric (showHex)

import EVM.Types (keccak')
Expand Down Expand Up @@ -93,15 +93,17 @@ instance ToJSON Transaction where
, "gasprice" .= gasprice
]

encodeCampaign :: C.Campaign -> ByteString
encodeCampaign C.Campaign{..} = encode
Campaign { _success = True
, _error = Nothing
, _tests = mapTest <$> tests
, seed = genDict.defSeed
--, coverage = mapKeys (("0x" ++) . (`showHex` "") . keccak') $ DF.toList <$> coverage
, gasInfo = toList gasInfo
}
encodeCampaign :: C.Campaign -> IO ByteString
encodeCampaign C.Campaign{..} = do
frozenCov <- mapM VU.freeze coverage
pure $ encode Campaign
{ _success = True
, _error = Nothing
, _tests = mapTest <$> tests
, seed = genDict.defSeed
, coverage = mapKeys (("0x" ++) . (`showHex` "") . keccak') $ VU.toList <$> frozenCov
, gasInfo = toList gasInfo
}

mapTest :: EchidnaTest -> Test
mapTest test =
Expand Down
2 changes: 1 addition & 1 deletion lib/Echidna/Transaction.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import Echidna.Types.Buffer (forceBuf, forceLit)
import Echidna.Types.Signature (SignatureMap, SolCall, ContractA, FunctionHash, MetadataCache, lookupBytecodeMetadata)
import Echidna.Types.Tx
import Echidna.Types.World (World(..))
import Echidna.Types.Campaign (Campaign(..))
import Echidna.Types.Campaign

hasSelfdestructed :: VM -> Addr -> Bool
hasSelfdestructed vm addr = addr `elem` vm._tx._substate._selfdestructs
Expand Down
11 changes: 7 additions & 4 deletions lib/Echidna/Types/Campaign.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Data.Text (Text)
import Echidna.ABI (GenDict, emptyDict)
import Echidna.Types
import Echidna.Types.Corpus
import Echidna.Types.Coverage (CoverageMap)
import Echidna.Types.Coverage (CoverageMap, FrozenCoverageMap)
import Echidna.Types.Test (EchidnaTest)
import Echidna.Types.Tx (Tx)

Expand Down Expand Up @@ -35,11 +35,14 @@ data CampaignConf = CampaignConf
, mutConsts :: MutationConsts Integer
}

type FrozenCampaign = GenericCampaign FrozenCoverageMap

type Campaign = GenericCampaign CoverageMap
-- | The state of a fuzzing campaign.
data Campaign = Campaign
data GenericCampaign a = Campaign
{ tests :: ![EchidnaTest]
-- ^ Tests being evaluated
, coverage :: !CoverageMap
, coverage :: !a
-- ^ Coverage captured (NOTE: we don't always record this)
, gasInfo :: !(Map Text (Gas, [Tx]))
-- ^ Worst case gas (NOTE: we don't always record this)
Expand All @@ -53,7 +56,7 @@ data Campaign = Campaign
-- ^ Number of times the callseq is called
}

defaultCampaign :: Campaign
defaultCampaign :: Monoid a => GenericCampaign a
defaultCampaign = Campaign mempty mempty mempty emptyDict False mempty 0

defaultTestLimit :: Int
Expand Down
8 changes: 7 additions & 1 deletion lib/Echidna/Types/Coverage.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,11 @@ type FrozenCoverageMap = Map ByteString (VU.Vector CoverageInfo)
scoveragePoints :: CoverageMap -> IO Int
scoveragePoints cm = do
let infos = Map.elems cm
sums <- forM infos $ V.foldl (\acc (e,_,_) -> if e == -1 then acc else acc + 1) 0
sums <- forM infos $ V.foldl' (\acc (e,_,_) -> if e == -1 then acc else acc + 1) 0
pure (sum sums)

scoveragePoints' :: FrozenCoverageMap -> Int
scoveragePoints' cm =
let infos = Map.elems cm
sums = VU.foldl' (\acc (e,_,_) -> if e == -1 then acc else acc + 1) 0 <$> infos
in sum sums
14 changes: 10 additions & 4 deletions lib/Echidna/UI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ import Echidna.Types.Tx (Tx)
import Echidna.Types.World (World)
import Echidna.UI.Report
import Echidna.Utility (timePrefix)
import qualified Data.Vector.Unboxed as VU

data UIEvent =
CampaignUpdated Campaign
| CampaignTimedout Campaign
CampaignUpdated FrozenCampaign
| CampaignTimedout FrozenCampaign
| CampaignCrashed String
| FetchCacheUpdated (Map Addr (Maybe Contract)) (Map Addr (Map W256 (Maybe W256)))

Expand Down Expand Up @@ -84,7 +85,7 @@ ui vm world ts dict initialCorpus = do
#ifdef INTERACTIVE_UI
Interactive -> do
bc <- liftIO $ newBChan 100
let updateUI e = readIORef ref >>= writeBChan bc . e
let updateUI e = readIORef ref >>= freezeCampaign >>= writeBChan bc . e
env <- ask
ticker <- liftIO $ forkIO $
-- run UI update every 100ms
Expand Down Expand Up @@ -144,7 +145,7 @@ ui vm world ts dict initialCorpus = do
pure (final, False)
case outputFormat of
JSON ->
liftIO . BS.putStr $ Echidna.Output.JSON.encodeCampaign final
liftIO $ BS.putStr =<< Echidna.Output.JSON.encodeCampaign final
Text -> do
camp <- ppCampaign final
liftIO . putStrLn $ camp
Expand All @@ -153,6 +154,11 @@ ui vm world ts dict initialCorpus = do
pure ()
pure final

freezeCampaign :: Campaign -> IO FrozenCampaign
freezeCampaign camp = do
frozenCov <- mapM VU.freeze camp.coverage
pure camp { coverage = frozenCov }

#ifdef INTERACTIVE_UI

vtyConfig :: IO Config
Expand Down
7 changes: 6 additions & 1 deletion lib/Echidna/UI/Report.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Echidna.Pretty (ppTxCall)
import Echidna.Types (Gas)
import Echidna.Types.Campaign
import Echidna.Types.Corpus (Corpus, corpusSize)
import Echidna.Types.Coverage (CoverageMap, scoveragePoints)
import Echidna.Types.Coverage (CoverageMap, scoveragePoints, FrozenCoverageMap, scoveragePoints')
import Echidna.Types.Test (EchidnaTest(..), TestState(..), TestType(..))
import Echidna.Types.Tx (Tx(..), TxCall(..), TxConf(..))
import Echidna.Types.Config
Expand Down Expand Up @@ -61,6 +61,11 @@ ppCoverage s = do
pure $ "Unique instructions: " <> show points
<> "\nUnique codehashes: " <> show (length s)

ppFrozenCoverage :: FrozenCoverageMap -> String
ppFrozenCoverage s =
"Unique instructions: " <> show (scoveragePoints' s)
<> "\nUnique codehashes: " <> show (length s)

-- | Pretty-print the corpus a 'Campaign' has obtained.
ppCorpus :: Corpus -> String
ppCorpus c = "Corpus size: " <> show (corpusSize c)
Expand Down
4 changes: 2 additions & 2 deletions lib/Echidna/UI/Widgets.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Data.Maybe (fromMaybe, isJust)

data UIState = UIState
{ status :: UIStateStatus
, campaign :: Campaign
, campaign :: FrozenCampaign
, fetchedContracts :: Map Addr (Maybe Contract)
, fetchedSlots :: Map Addr (Map W256 (Maybe W256))
, fetchedDialog :: B.Dialog ()
Expand Down Expand Up @@ -120,7 +120,7 @@ summaryWidget uiState =
<=>
str ("Seed: " ++ show c.genDict.defSeed)
<=>
str "0" -- (ppCoverage c.coverage)
str (ppFrozenCoverage c.coverage)
<=>
str (ppCorpus c.corpus)
rightSide = fetchCacheWidget uiState.fetchedContracts uiState.fetchedSlots
Expand Down
4 changes: 2 additions & 2 deletions src/test/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Data.Function ((&))
import Data.IORef
import Data.List.NonEmpty (NonEmpty(..))
import Data.List.Split (splitOn)
import Data.Map (fromList, lookup, empty)
import Data.Map (fromList, lookup)
import Data.Maybe (isJust)
import Data.Text (Text, pack)
import Data.SemVer (Version, version, fromText)
Expand All @@ -47,7 +47,7 @@ import Echidna.Solidity (loadSolTests, compileContracts, selectSourceCache)
import Echidna.Test (checkETest)
import Echidna.Types (Gas)
import Echidna.Types.Config (Env(..), EConfig(..), EConfigWithUsage(..))
import Echidna.Types.Campaign (Campaign(..), CampaignConf(..))
import Echidna.Types.Campaign
import Echidna.Types.Signature (ContractName)
import Echidna.Types.Solidity (SolConf(..))
import Echidna.Types.Test
Expand Down
12 changes: 6 additions & 6 deletions src/test/Tests/Config.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Tests.Config (configTests) where

import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, assertBool, (@?=), assertFailure)
import Test.Tasty.HUnit (testCase, assertBool, assertFailure)

import Control.Lens (sans)
import Control.Monad (void)
Expand All @@ -12,16 +12,17 @@ import Echidna.Types.Config (EConfigWithUsage(..), EConfig(..))
import Echidna.Types.Campaign (CampaignConf(..))
import Echidna.Types.Tx (TxConf(..))
import Echidna.Config (defaultConfig, parseConfig)
import Data.Maybe (isJust)

configTests :: TestTree
configTests = testGroup "Configuration tests" $
[ testCase file . void $ parseConfig file | file <- files ] ++
[ {-testCase "parse \"coverage: true\"" $ do
[ testCase "parse \"coverage: true\"" $ do
config <- (.econfig) <$> parseConfig "coverage/test.yaml"
assertCoverage config $ Just mempty
assertBool "" $ isJust config.campaignConf.knownCoverage
, testCase "coverage enabled by default" $
assertCoverage defaultConfig $ Just mempty
,-} testCase "default.yaml" $ do
assertBool "" $ isJust defaultConfig.campaignConf.knownCoverage
, testCase "default.yaml" $ do
EConfigWithUsage _ bad unset <- parseConfig "basic/default.yaml"
assertBool ("unused options: " ++ show bad) $ null bad
let unset' = unset & sans "seed"
Expand All @@ -38,4 +39,3 @@ configTests = testGroup "Configuration tests" $
Left _ -> pure ()
]
where files = ["basic/config.yaml", "basic/default.yaml"]
assertCoverage config value = config.campaignConf.knownCoverage @?= value
4 changes: 2 additions & 2 deletions src/test/Tests/Coverage.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import Common (testContract, passed, countCorpus)

coverageTests :: TestTree
coverageTests = testGroup "Coverage tests"
[
[
-- single.sol is really slow and kind of unstable. it also messes up travis.
-- testContract "coverage/single.sol" (Just "coverage/test.yaml")
-- [ ("echidna_state failed", solved "echidna_state") ]
-- testContract' "coverage/multi.sol" Nothing Nothing (Just "coverage/test.yaml") False
-- [ ("echidna_state3 failed", solved "echidna_state3") ]
testContract "coverage/boolean.sol" (Just "coverage/boolean.yaml")
[ ("echidna_true failed", passed "echidna_true")
, ("unexpected corpus count ", countCorpus 4)]
, ("unexpected corpus count ", countCorpus 1)]

]
2 changes: 1 addition & 1 deletion src/test/Tests/Seed.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Test.Tasty.HUnit (testCase, assertBool)
import Common (runContract, overrideQuiet)
import Data.Function ((&))
import Echidna.Types.Config (EConfig(..))
import Echidna.Types.Campaign (Campaign(..), CampaignConf(..))
import Echidna.Types.Campaign
import Echidna.Mutator.Corpus (defaultMutationConsts)
import Echidna.Config (defaultConfig)

Expand Down
2 changes: 1 addition & 1 deletion tests/solidity/coverage/boolean.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
coverage: true
seqLen: 1
seqLen: 10
testLimit: 5000

0 comments on commit d198906

Please sign in to comment.