Skip to content

Commit

Permalink
RPC improvements (chain id, config options)
Browse files Browse the repository at this point in the history
  • Loading branch information
arcz committed Apr 6, 2023
1 parent 1a32fde commit efbdadd
Show file tree
Hide file tree
Showing 16 changed files with 385 additions and 298 deletions.
2 changes: 1 addition & 1 deletion lib/Echidna.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import EVM.ABI (AbiValue(AbiAddress))
import EVM.Solidity (SolcContract(..))

import Echidna.ABI
import Echidna.Etheno (loadEtheno, extractFromEtheno)
import Echidna.Output.Corpus
import Echidna.Processor
import Echidna.RPC (loadEtheno, extractFromEtheno)
import Echidna.Solidity
import Echidna.Test (createTests)
import Echidna.Types.Campaign hiding (corpus)
Expand Down
15 changes: 9 additions & 6 deletions lib/Echidna/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ instance FromJSON EConfigWithUsage where
<*> testConfParser
<*> txConfParser
<*> (UIConf <$> v ..:? "timeout" <*> formatParser)
<*> v ..:? "rpcUrl"
<*> v ..:? "rpcBlock"
where
useKey k = modify' $ insert k
x ..:? k = useKey k >> lift (x .:? k)
Expand Down Expand Up @@ -84,16 +86,17 @@ instance FromJSON EConfigWithUsage where
pure $ TestConf classify (const psender)

campaignConfParser = CampaignConf
<$> v ..:? "testLimit" ..!= defaultTestLimit
<*> v ..:? "stopOnFail" ..!= False
<$> v ..:? "testLimit" ..!= defaultTestLimit
<*> v ..:? "stopOnFail" ..!= False
<*> v ..:? "estimateGas" ..!= False
<*> v ..:? "seqLen" ..!= defaultSequenceLength
<*> v ..:? "seqLen" ..!= defaultSequenceLength
<*> v ..:? "shrinkLimit" ..!= defaultShrinkLimit
<*> (v ..:? "coverage" <&> \case Just False -> Nothing; _ -> Just mempty)
<*> v ..:? "seed"
<*> v ..:? "dictFreq" ..!= 0.40
<*> v ..:? "corpusDir" ..!= Nothing
<*> v ..:? "mutConsts" ..!= defaultMutationConsts
<*> v ..:? "dictFreq" ..!= 0.40
<*> v ..:? "corpusDir" ..!= Nothing
<*> v ..:? "mutConsts" ..!= defaultMutationConsts
<*> v ..:? "coverageReport" ..!= True

solConfParser = SolConf
<$> v ..:? "contractAddr" ..!= defaultContractAddr
Expand Down
2 changes: 1 addition & 1 deletion lib/Echidna/Fetch.hs → lib/Echidna/Deploy.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Echidna.Fetch where
module Echidna.Deploy where

import Control.Monad.Catch (MonadThrow(..), throwM)
import Control.Monad.State.Strict (execStateT, MonadIO)
Expand Down
179 changes: 179 additions & 0 deletions lib/Echidna/Etheno.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
{-# LANGUAGE GADTs #-}

module Echidna.Etheno where

import Prelude hiding (Word)

import Control.Exception (Exception)
import Control.Lens
import Control.Monad (void)
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.Fail qualified as M (MonadFail(..))
import Control.Monad.State.Strict (MonadState, get, put, execStateT)
import Data.Aeson (FromJSON(..), (.:), withObject, eitherDecodeFileStrict)
import Data.ByteString.Base16 qualified as BS16 (decode)
import Data.ByteString.Char8 (ByteString)
import Data.ByteString.Char8 qualified as BS
import Data.ByteString.Lazy qualified as LBS
import Data.Text qualified as T (drop)
import Data.Text.Encoding (encodeUtf8)
import Data.Map (member)
import Data.Vector qualified as V (fromList, toList)
import Text.Read (readMaybe)

import EVM
import EVM.ABI (AbiType(..), AbiValue(..), decodeAbiValue, selector)
import EVM.Exec (exec)
import EVM.Types (Addr, W256, Expr(ConcreteBuf))

import Echidna.Exec
import Echidna.Transaction
import Echidna.Types.Signature (SolSignature)
import Echidna.ABI (encodeSig)
import Echidna.Types (fromEVM)
import Echidna.Types.Tx (TxCall(..), Tx(..), makeSingleTx, createTxWithValue, unlimitedGasPerBlock)
import Data.Set (Set)

-- | During initialization we can either call a function or create an account or contract
data Etheno
-- | Registers an address with the echidna runtime
= AccountCreated Addr
-- | A contract was constructed on the blockchain
| ContractCreated Addr Addr Integer Integer ByteString W256
-- | A contract function was executed
| FunctionCall Addr Addr Integer Integer ByteString W256
-- | A new block was mined contract
| BlockMined Integer Integer
deriving (Eq, Show)

instance FromJSON Etheno where
parseJSON = withObject "Etheno" $ \v -> do
(ev :: String) <- v .: "event"
let gu = maybe (M.fail "could not parse gas_used") pure . readMaybe =<< v .: "gas_used"
gp = maybe (M.fail "could not parse gas_price") pure . readMaybe =<< v .: "gas_price"
ni = maybe (M.fail "could not parse number_increase") pure . readMaybe =<< v .: "number_increment"
ti = maybe (M.fail "could not parse timestamp_increase") pure . readMaybe =<< v .: "timestamp_increment"
case ev of
"AccountCreated" -> AccountCreated <$> v .: "address"
"ContractCreated" -> ContractCreated <$> v .: "from"
<*> v .: "contract_address"
<*> gu
<*> gp
<*> (decode =<< (v .: "data"))
<*> v .: "value"
"FunctionCall" -> FunctionCall <$> v .: "from"
<*> v .: "to"
<*> gu
<*> gp
<*> (decode =<< (v .: "data"))
<*> v .: "value"
"BlockMined" -> BlockMined <$> ni
<*> ti

_ -> M.fail "event should be one of \"AccountCreated\", \"ContractCreated\", or \"FunctionCall\""
where decode x = case BS16.decode . encodeUtf8 . T.drop 2 $ x of
Right a -> pure a
Left e -> M.fail $ "could not decode hexstring: " <> e


-- | Handler for parsing errors
-- TODO: make this a better sum type
newtype EthenoException = EthenoException String

instance Show EthenoException where
show (EthenoException e) = "Error parsing Etheno initialization file: " ++ e

instance Exception EthenoException

loadEtheno :: FilePath -> IO [Etheno]
loadEtheno fp = do
bs <- eitherDecodeFileStrict fp
case bs of
(Left e) -> throwM $ EthenoException e
(Right (ethenoInit :: [Etheno])) -> return ethenoInit

extractFromEtheno :: [Etheno] -> Set SolSignature -> [Tx]
extractFromEtheno ess ss = case ess of
(BlockMined ni ti :es) ->
Tx NoCall 0 0 0 0 0 (fromInteger ti, fromInteger ni) : extractFromEtheno es ss
(c@FunctionCall{} :es) ->
concatMap (`matchSignatureAndCreateTx` c) ss ++ extractFromEtheno es ss
(_:es) -> extractFromEtheno es ss
_ -> []

matchSignatureAndCreateTx :: SolSignature -> Etheno -> [Tx]
matchSignatureAndCreateTx ("", []) _ = [] -- Not sure if we should match this.
matchSignatureAndCreateTx (s,ts) (FunctionCall a d _ _ bs v) =
if BS.take 4 bs == selector (encodeSig (s,ts))
then makeSingleTx a d v $ SolCall (s, fromTuple $ decodeAbiValue t (LBS.fromStrict $ BS.drop 4 bs))
else []
where t = AbiTupleType (V.fromList ts)
fromTuple (AbiTuple xs) = V.toList xs
fromTuple _ = []
matchSignatureAndCreateTx _ _ = []

-- | Main function: takes a filepath where the initialization sequence lives and returns
-- | the initialized VM along with a list of Addr's to put in GenConf
loadEthenoBatch :: Bool -> FilePath -> IO VM
loadEthenoBatch ffi fp = do
bs <- eitherDecodeFileStrict fp
case bs of
Left e -> throwM $ EthenoException e
Right (ethenoInit :: [Etheno]) -> do
-- Execute contract creations and initial transactions,
let initVM = mapM execEthenoTxs ethenoInit
execStateT initVM (initialVM ffi)

initAddress :: MonadState VM m => Addr -> m ()
initAddress addr = do
cs <- use (env . EVM.contracts)
if addr `member` cs then pure ()
else env . EVM.contracts . at addr .= Just account
where
account =
initialContract (RuntimeCode (ConcreteRuntimeCode mempty))
& set nonce 0
& set balance 100000000000000000000 -- default balance for EOAs in etheno

crashWithQueryError :: (MonadState VM m, MonadFail m, MonadThrow m) => Query -> Etheno -> m ()
crashWithQueryError q et =
case (q, et) of
(PleaseFetchContract addr _, FunctionCall f t _ _ _ _) ->
error ("Address " ++ show addr ++ " was used during function call from " ++ show f ++ " to " ++ show t ++ " but it was never defined as EOA or deployed as a contract")
(PleaseFetchContract addr _, ContractCreated f t _ _ _ _) ->
error ("Address " ++ show addr ++ " was used during the contract creation of " ++ show t ++ " from " ++ show f ++ " but it was never defined as EOA or deployed as a contract")
(PleaseFetchSlot slot _ _, FunctionCall f t _ _ _ _) ->
error ("Slot " ++ show slot ++ " was used during function call from " ++ show f ++ " to " ++ show t ++ " but it was never loaded")
(PleaseFetchSlot slot _ _, ContractCreated f t _ _ _ _) ->
error ("Slot " ++ show slot ++ " was used during the contract creation of " ++ show t ++ " from " ++ show f ++ " but it was never loaded")
_ -> error $ show (q, et)

-- | Takes a list of Etheno transactions and loads them into the VM, returning the
-- | address containing echidna tests
execEthenoTxs :: (MonadState VM m, MonadFail m, MonadThrow m) => Etheno -> m ()
execEthenoTxs et = do
setupEthenoTx et
vm <- get
res <- fromEVM exec
case (res, et) of
(_ , AccountCreated _) -> return ()
(Reversion, _) -> void $ put vm
(VMFailure (Query q), _) -> crashWithQueryError q et
(VMFailure x, _) -> vmExcept x >> M.fail "impossible"
(VMSuccess (ConcreteBuf bc),
ContractCreated _ ca _ _ _ _) -> do
env . contracts . at ca . _Just . contractcode .= InitCode mempty mempty
fromEVM (replaceCodeOfSelf (RuntimeCode (ConcreteRuntimeCode bc)) >> loadContract ca)
pure ()
_ -> pure ()

-- | For an etheno txn, set up VM to execute txn
setupEthenoTx :: MonadState VM m => Etheno -> m ()
setupEthenoTx (AccountCreated f) =
initAddress f -- TODO: improve etheno to include initial balance
setupEthenoTx (ContractCreated f c _ _ d v) =
setupTx $ createTxWithValue d f c unlimitedGasPerBlock v (1, 1)
setupEthenoTx (FunctionCall f t _ _ d v) =
setupTx $ Tx (SolCalldata d) f t unlimitedGasPerBlock 0 v (1, 1)
setupEthenoTx (BlockMined n t) =
setupTx $ Tx NoCall 0 0 0 0 0 (fromInteger t, fromInteger n)
52 changes: 12 additions & 40 deletions lib/Echidna/Exec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@
module Echidna.Exec where

import Control.Lens
import Control.Monad (when)
import Control.Monad.Catch (MonadThrow(..), catchAll, SomeException)
import Control.Monad (unless)
import Control.Monad.Catch (MonadThrow(..))
import Control.Monad.State.Strict (MonadState(get, put), execState, runStateT, MonadIO(liftIO))
import Control.Monad.Reader (MonadReader, asks)
import Data.IORef (readIORef, atomicWriteIORef)
import Data.Map qualified as M
import Data.Map qualified as Map
import Data.Maybe (fromMaybe)
import Data.Set qualified as S
import Data.Text qualified as Text
import Data.Text qualified as T
import Data.Vector qualified as V
import Text.Read (readMaybe)
import System.Environment (lookupEnv)
import System.Process (readProcessWithExitCode)

import EVM hiding (Env, cache, contract, tx, value)
Expand All @@ -28,13 +25,14 @@ import EVM.Fetch qualified
import EVM.Types (Expr(ConcreteBuf, Lit), hexText)

import Echidna.Events (emptyEvents)
import Echidna.RPC (safeFetchContractFrom, safeFetchSlotFrom)
import Echidna.Transaction
import Echidna.Types (ExecException(..), Gas, fromEVM)
import Echidna.Types (ExecException(..), Gas, fromEVM, emptyAccount)
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)
import Echidna.Types.Config (Env(..), EConfig(..), UIConf(..), OperationMode(..), OutputFormat(Text))
import Echidna.Types.Config (Env(..), EConfig(..))
import Echidna.Types.Solidity (SolConf(..))
import Echidna.Utility (timePrefix)

Expand All @@ -57,9 +55,6 @@ getQuery :: VMResult -> Maybe Query
getQuery (VMFailure (Query q)) = Just q
getQuery _ = Nothing

emptyAccount :: Contract
emptyAccount = initialContract (RuntimeCode (ConcreteRuntimeCode mempty))

-- | Matches execution errors that just cause a reversion.
pattern Reversion :: VMResult
pattern Reversion <- VMFailure (classifyError -> RevertE)
Expand Down Expand Up @@ -91,6 +86,10 @@ execTxWith l onErr executeTx tx = do
pure (vmResult, gasLeftBeforeTx - gasLeftAfterTx)
where
runFully = do
config <- asks (.cfg)
-- TODO: Is the latest block a good default? It makes fuzzing hard to reproduce. Rethink this.
let rpcBlock = maybe EVM.Fetch.Latest (EVM.Fetch.BlockNumber . fromIntegral) config.rpcBlock

vmResult <- executeTx
-- For queries, we halt execution because the VM needs some additional
-- information from the outside. We provide this information and resume
Expand All @@ -106,9 +105,8 @@ execTxWith l onErr executeTx tx = do
l %= execState (continuation emptyAccount)
Nothing -> do
logMsg $ "INFO: Performing RPC: " <> show q
getRpcUrl >>= \case
case config.rpcUrl of
Just rpcUrl -> do
rpcBlock <- getRpcBlock
ret <- liftIO $ safeFetchContractFrom rpcBlock rpcUrl addr
case ret of
-- TODO: fix hevm to not return an empty contract in case of an error
Expand Down Expand Up @@ -145,9 +143,8 @@ execTxWith l onErr executeTx tx = do
Just Nothing -> l %= execState (continuation 0)
Nothing -> do
logMsg $ "INFO: Performing RPC: " <> show q
getRpcUrl >>= \case
case config.rpcUrl of
Just rpcUrl -> do
rpcBlock <- getRpcBlock
ret <- liftIO $ safeFetchSlotFrom rpcBlock rpcUrl addr slot
case ret of
Just value -> do
Expand Down Expand Up @@ -177,30 +174,6 @@ execTxWith l onErr executeTx tx = do

-- No queries to answer, the tx is fully executed and the result is final
_ -> pure vmResult
where
-- TODO: Currently, for simplicity we get those values from env vars.
-- Make it posible to pass through the config file and CLI
getRpcUrl = liftIO $ do
val <- lookupEnv "ECHIDNA_RPC_URL"
pure (Text.pack <$> val)

getRpcBlock = liftIO $ do
-- TODO: Is the latest block a good default? It makes fuzzing hard to
-- reproduce. Rethink this.
val <- lookupEnv "ECHIDNA_RPC_BLOCK"
pure $ maybe EVM.Fetch.Latest EVM.Fetch.BlockNumber (val >>= readMaybe)

-- TODO: temporary solution, handle errors gracefully
safeFetchContractFrom rpcBlock rpcUrl addr =
catchAll
(EVM.Fetch.fetchContractFrom rpcBlock rpcUrl addr)
(\(_e :: SomeException) -> pure $ Just emptyAccount)

-- TODO: temporary solution, handle errors gracefully
safeFetchSlotFrom rpcBlock rpcUrl addr slot =
catchAll
(EVM.Fetch.fetchSlotFrom rpcBlock rpcUrl addr slot)
(\(_e :: SomeException) -> pure $ Just 0)

-- | Handles reverts, failures and contract creations that might be the result
-- (`vmResult`) of executing transaction `tx`.
Expand Down Expand Up @@ -233,8 +206,7 @@ execTxWith l onErr executeTx tx = do
logMsg :: (MonadIO m, MonadReader Env m) => String -> m ()
logMsg msg = do
cfg <- asks (.cfg)
operationMode <- asks (.cfg.uiConf.operationMode)
when (operationMode == NonInteractive Text && not cfg.solConf.quiet) $ liftIO $ do
unless (cfg.solConf.quiet) $ liftIO $ do
time <- timePrefix
putStrLn $ time <> msg

Expand Down
Loading

0 comments on commit efbdadd

Please sign in to comment.