Skip to content

Commit

Permalink
Refactor (#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
arcz committed Jan 25, 2024
1 parent 2cf85c3 commit f964ba6
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 333 deletions.
26 changes: 22 additions & 4 deletions lib/Echidna.hs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module Echidna where

import Control.Concurrent (newChan)
import Control.Monad.Catch (MonadThrow(..))
import Control.Monad.ST (RealWorld)
import Data.IORef (writeIORef)
import Data.IORef (writeIORef, newIORef)
import Data.List (find)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
Expand All @@ -12,8 +13,9 @@ import System.FilePath ((</>))

import EVM (cheatCode)
import EVM.ABI (AbiValue(AbiAddress))
import EVM.Dapp (DappInfo(..))
import EVM.Solidity (SolcContract(..))
import EVM.Dapp (DappInfo(..), dappInfo)
import EVM.Fetch qualified
import EVM.Solidity (SolcContract(..), BuildOutput)
import EVM.Types hiding (Env)

import Echidna.ABI
Expand Down Expand Up @@ -52,7 +54,7 @@ prepareContract env solFiles specifiedContract seed = do
let solConf = env.cfg.solConf
contracts = Map.elems env.dapp.solcByName

-- compile and load contracts
-- deploy contracts
(vm, funs, testNames, signatureMap) <- loadSpecified env specifiedContract contracts

-- run processors
Expand Down Expand Up @@ -110,3 +112,19 @@ loadInitialCorpus env world = do
pure (ctxs1 ++ ctxs2)

pure $ persistedCorpus ++ ethenoCorpus

mkEnv :: EConfig -> BuildOutput -> IO Env
mkEnv cfg buildOutput = do
fetchContractCache <- newIORef mempty
fetchSlotCache <- newIORef mempty
codehashMap <- newIORef mempty
chainId <- maybe (pure Nothing) EVM.Fetch.fetchChainIdFrom cfg.rpcUrl
eventQueue <- newChan
coverageRef <- newIORef mempty
corpusRef <- newIORef mempty
testsRef <- newIORef mempty
-- TODO put in real path
let dapp = dappInfo "/" buildOutput
pure $ Env { cfg, dapp, codehashMap, fetchContractCache, fetchSlotCache
, chainId, eventQueue, coverageRef, corpusRef, testsRef
}
8 changes: 4 additions & 4 deletions lib/Echidna/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import Data.Yaml qualified as Y

import EVM.Types (VM(..), W256)

import Echidna.Mutator.Corpus (defaultMutationConsts)
import Echidna.Test
import Echidna.Types.Campaign
import Echidna.Mutator.Corpus (defaultMutationConsts)
import Echidna.Output.Source (CoverageFileType(..))
import Echidna.Types.Config
import Echidna.Types.Coverage (CoverageFileType(..))
import Echidna.Types.Solidity
import Echidna.Types.Tx (TxConf(TxConf), maxGasPerBlock, defaultTimeDelay, defaultBlockDelay)
import Echidna.Types.Test (TestConf(..))
import Echidna.Types.Config
import Echidna.Types.Tx (TxConf(TxConf), maxGasPerBlock, defaultTimeDelay, defaultBlockDelay)

instance FromJSON EConfig where
-- retrieve the config from the key usage annotated parse
Expand Down
2 changes: 1 addition & 1 deletion lib/Echidna/Exec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import EVM.Format (hexText)
import EVM.Types hiding (Env)

import Echidna.Events (emptyEvents)
import Echidna.RPC (safeFetchContractFrom, safeFetchSlotFrom)
import Echidna.Onchain (safeFetchContractFrom, safeFetchSlotFrom)
import Echidna.SourceMapping (lookupUsingCodehashOrInsert)
import Echidna.Symbolic (forceBuf)
import Echidna.Transaction
Expand Down
202 changes: 202 additions & 0 deletions lib/Echidna/Onchain.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
{-# LANGUAGE DeriveAnyClass #-}

module Echidna.Onchain where

import Control.Exception (catch)
import Data.Aeson (ToJSON, FromJSON, ToJSONKey(toJSONKey))
import Data.Aeson qualified as JSON
import Data.Aeson.Types (toJSONKeyText)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.UTF8 qualified as UTF8
import Data.Functor ((<&>))
import Data.IORef (writeIORef, readIORef)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe (isJust, fromJust, fromMaybe)
import Data.Text qualified as Text
import Data.Text (Text)
import Data.Vector qualified as Vector
import Data.Word (Word64)
import Etherscan qualified
import GHC.Generics (Generic)
import Network.HTTP.Simple (HttpException)
import Optics (view)
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.Environment (lookupEnv)
import System.FilePath ((</>))
import Text.Read (readMaybe)

import EVM (initialContract, bytecode)
import EVM.Fetch qualified
import EVM.Solidity (SourceCache(..), SolcContract (..))
import EVM.Types hiding (Env)

import Echidna.Orphans.JSON ()
import Echidna.Symbolic (forceWord, forceBuf)
import Echidna.Types (emptyAccount)
import Echidna.Types.Campaign (CampaignConf(..))
import Echidna.Types.Config (Env(..), EConfig(..))
import Echidna.Output.Source (saveCoverages)
import Control.Monad (when, forM_)

rpcUrlEnv :: IO (Maybe Text)
rpcUrlEnv = do
val <- lookupEnv "ECHIDNA_RPC_URL"
pure (Text.pack <$> val)

rpcBlockEnv :: IO (Maybe Word64)
rpcBlockEnv = do
val <- lookupEnv "ECHIDNA_RPC_BLOCK"
pure (val >>= readMaybe)

-- TODO: temporary solution, handle errors gracefully
safeFetchContractFrom :: EVM.Fetch.BlockNumber -> Text -> Addr -> IO (Maybe Contract)
safeFetchContractFrom rpcBlock rpcUrl addr =
catch
(EVM.Fetch.fetchContractFrom rpcBlock rpcUrl addr)
(\(_ :: HttpException) -> pure $ Just emptyAccount)

-- TODO: temporary solution, handle errors gracefully
safeFetchSlotFrom :: EVM.Fetch.BlockNumber -> Text -> Addr -> W256 -> IO (Maybe W256)
safeFetchSlotFrom rpcBlock rpcUrl addr slot =
catch
(EVM.Fetch.fetchSlotFrom rpcBlock rpcUrl addr slot)
(\(_ :: HttpException) -> pure $ Just 0)

data FetchedContractData = FetchedContractData
{ runtimeCode :: ByteString
, nonce :: Maybe W64
, balance :: W256
}
deriving (Generic, ToJSON, FromJSON, Show)

instance ToJSONKey W256 where
toJSONKey = toJSONKeyText (Text.pack . show)

fromFetchedContractData :: FetchedContractData -> Contract
fromFetchedContractData contractData =
(initialContract (RuntimeCode (ConcreteRuntimeCode contractData.runtimeCode)))
{ nonce = contractData.nonce
, balance = Lit contractData.balance
, external = True
}

toFetchedContractData :: Contract -> FetchedContractData
toFetchedContractData contract =
let code = case contract.code of
RuntimeCode (ConcreteRuntimeCode c) -> c
_ -> error "unexpected code"
in FetchedContractData
{ runtimeCode = code
, nonce = contract.nonce
, balance = forceWord contract.balance
}

-- | Try to load the persisted RPC cache.
-- TODO: we use the corpus dir for now, think where to place it
loadRpcCache :: Env -> IO ()
loadRpcCache Env { cfg, fetchContractCache, fetchSlotCache } =
case cfg.campaignConf.corpusDir of
Nothing -> pure ()
Just dir -> do
let cache_dir = dir </> "cache"
createDirectoryIfMissing True cache_dir
case cfg.rpcBlock of
Just block -> do
parsedContracts :: Maybe (Map Addr FetchedContractData) <-
readFileIfExists (cache_dir </> "block_" <> show block <> "_fetch_cache_contracts.json")
<&> (>>= JSON.decodeStrict)
parsedSlots :: Maybe (Map Addr (Map W256 (Maybe W256))) <-
readFileIfExists (cache_dir </> "block_" <> show block <> "_fetch_cache_slots.json")
<&> (>>= JSON.decodeStrict)
writeIORef fetchContractCache (maybe mempty (Map.map (Just . fromFetchedContractData)) parsedContracts)
writeIORef fetchSlotCache (fromMaybe mempty parsedSlots)
Nothing ->
pure ()

readFileIfExists :: FilePath -> IO (Maybe BS.ByteString)
readFileIfExists path = do
exists <- doesFileExist path
if exists then Just <$> BS.readFile path else pure Nothing

-- | "Reverse engineer" the SolcContract and SourceCache structures for the
-- code fetched from the outside
externalSolcContract :: Addr -> Contract -> IO (Maybe (SourceCache, SolcContract))
externalSolcContract addr c = do
let runtimeCode = forceBuf $ fromJust $ view bytecode c
putStr $ "Fetching Solidity source for contract at address " <> show addr <> "... "
srcRet <- Etherscan.fetchContractSource addr
putStrLn $ if isJust srcRet then "Success!" else "Error!"
putStr $ "Fetching Solidity source map for contract at address " <> show addr <> "... "
srcmapRet <- Etherscan.fetchContractSourceMap addr
putStrLn $ if isJust srcmapRet then "Success!" else "Error!"
pure $ do
src <- srcRet
(_, srcmap) <- srcmapRet
let
files = Map.singleton 0 (show addr, UTF8.fromString src.code)
sourceCache = SourceCache
{ files
, lines = Vector.fromList . BS.split 0xa . snd <$> files
, asts = mempty
}
solcContract = SolcContract
{ runtimeCode = runtimeCode
, creationCode = mempty
, runtimeCodehash = keccak' runtimeCode
, creationCodehash = keccak' mempty
, runtimeSrcmap = mempty
, creationSrcmap = srcmap
, contractName = src.name
, constructorInputs = [] -- error "TODO: mkConstructor abis TODO"
, abiMap = mempty -- error "TODO: mkAbiMap abis"
, eventMap = mempty -- error "TODO: mkEventMap abis"
, errorMap = mempty -- error "TODO: mkErrorMap abis"
, storageLayout = Nothing
, immutableReferences = mempty
}
pure (sourceCache, solcContract)

-- TODO: This should happen continuously event-based
saveRpcCache :: Env -> IO ()
saveRpcCache env = do
contractsCache <- readIORef env.fetchContractCache
slotsCache <- readIORef env.fetchSlotCache
case env.cfg.campaignConf.corpusDir of
Nothing -> pure ()
Just dir -> do
let cacheDir = dir </> "cache"
case env.cfg.rpcBlock of
Just block -> do
-- Save fetched data, it's okay to override as the cache only grows
JSON.encodeFile (cacheDir </> "block_" <> show block <> "_fetch_cache_contracts.json")
(toFetchedContractData <$> Map.mapMaybe id contractsCache)
JSON.encodeFile (cacheDir </> "block_" <> show block <> "_fetch_cache_slots.json")
slotsCache
Nothing ->
pure ()

saveCoverageReport :: Env -> Int -> IO ()
saveCoverageReport env runId = do
case env.cfg.campaignConf.corpusDir of
Nothing -> pure ()
Just dir -> do
-- coverage reports for external contracts, we only support
-- Ethereum Mainnet for now
when (env.chainId == Just 1) $ do
contractsCache <- readIORef env.fetchContractCache
forM_ (Map.toList contractsCache) $ \(addr, mc) ->
case mc of
Just contract -> do
r <- externalSolcContract addr contract
case r of
Just (externalSourceCache, solcContract) -> do
let dir' = dir </> show addr
saveCoverages env
runId
dir'
externalSourceCache
[solcContract]
Nothing -> pure ()
Nothing -> pure ()
30 changes: 10 additions & 20 deletions lib/Echidna/Output/Source.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ module Echidna.Output.Source where

import Prelude hiding (writeFile)

import Data.Aeson (ToJSON(..), FromJSON(..), withText)
import Data.ByteString qualified as BS
import Data.Foldable
import Data.IORef (readIORef)
import Data.List (nub, sort)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Sequence qualified as Seq
import Data.Set qualified as S
import Data.Text (Text, pack, toLower)
import Data.Text (Text, pack)
import Data.Text qualified as T
import Data.Text.Encoding (decodeUtf8)
import Data.Text.IO (writeFile)
Expand All @@ -27,19 +27,22 @@ import Text.Printf (printf)
import EVM.Dapp (srcMapCodePos)
import EVM.Solidity (SourceCache(..), SrcMap, SolcContract(..))

import Echidna.Types.Coverage (OpIx, unpackTxResults, CoverageMap)
import Echidna.Types.Campaign (CampaignConf(..))
import Echidna.Types.Config (Env(..), EConfig(..))
import Echidna.Types.Coverage (OpIx, unpackTxResults, CoverageMap, CoverageFileType (..))
import Echidna.Types.Tx (TxResult(..))

saveCoverages
:: [CoverageFileType]
:: Env
-> Int
-> FilePath
-> SourceCache
-> [SolcContract]
-> CoverageMap
-> IO ()
saveCoverages fileTypes seed d sc cs s =
mapM_ (\ty -> saveCoverage ty seed d sc cs s) fileTypes
saveCoverages env seed d sc cs = do
let fileTypes = env.cfg.campaignConf.coverageFormats
coverage <- readIORef env.coverageRef
mapM_ (\ty -> saveCoverage ty seed d sc cs coverage) fileTypes

saveCoverage
:: CoverageFileType
Expand All @@ -56,19 +59,6 @@ saveCoverage fileType seed d sc cs covMap = do
createDirectoryIfMissing True d
writeFile fn cc

data CoverageFileType = Lcov | Html | Txt deriving (Eq, Show)

instance ToJSON CoverageFileType where
toJSON = toJSON . show

instance FromJSON CoverageFileType where
parseJSON = withText "CoverageFileType" $ readFn . toLower where
readFn "lcov" = pure Lcov
readFn "html" = pure Html
readFn "text" = pure Txt
readFn "txt" = pure Txt
readFn _ = fail "could not parse CoverageFileType"

coverageFileExtension :: CoverageFileType -> String
coverageFileExtension Lcov = ".lcov"
coverageFileExtension Html = ".html"
Expand Down

0 comments on commit f964ba6

Please sign in to comment.