From 6956030330b5f0b4bba4202c2c3701a11a533a78 Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:50:01 +0200 Subject: [PATCH] Add missing documentation (#1298) * initial documentation of shrink * expanded documentation of generation and mutation --- lib/Echidna/ABI.hs | 20 ++++++++++++++++---- lib/Echidna/Shrink.hs | 28 +++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/Echidna/ABI.hs b/lib/Echidna/ABI.hs index e5269c967..02ce90e45 100644 --- a/lib/Echidna/ABI.hs +++ b/lib/Echidna/ABI.hs @@ -153,14 +153,20 @@ mkDictValues = fromValue (AbiInt _ n) = Just (fromIntegral n) fromValue _ = Nothing --- Generation (synthesis) - +-- Generate a random integer using a pow scale: getRandomPow :: (MonadRandom m) => Int -> m Integer getRandomPow n = if n <= 0 then return 0 else do + -- generate uniformly a number from 20 to n mexp <- getRandomR (20, n) + -- generate uniformly a number from the range 2 ^ (mexp / 2) to 2 ^ mexp getRandomR (2 ^ (mexp `div` 2), 2 ^ mexp) +-- Generate a random unsigned integer with the following distribution: +-- * 9% (2/21) uniformly from 0 to 1024 +-- * 76% (16/21) uniformly from 0 to 2 ^ n - 5 +-- * 9% (2/21) uniformly from 2 ^ n - 5 to 2 ^ n - 1. +-- * 4% (1/21) using the getRandomPow function getRandomUint :: MonadRandom m => Int -> m Integer getRandomUint n = join $ Random.weighted @@ -170,6 +176,9 @@ getRandomUint n = , (getRandomPow (n - 5), 1) ] +-- | Generate a random signed integer with the following distribution: +-- * 10% uniformly from the range -1023 to 1023. +-- * 90% uniformly from the range -1 * 2 ^ n to 2 ^ (n - 1). getRandomInt :: MonadRandom m => Int -> m Integer getRandomInt n = getRandomR =<< Random.weighted @@ -310,8 +319,8 @@ mutateAbiValue = \case \case 0 -> fixAbiInt n <$> mutateNum x _ -> pure $ AbiInt n x - AbiAddress x -> pure $ AbiAddress x - AbiBool _ -> genAbiValue AbiBoolType + AbiAddress x -> pure $ AbiAddress x -- Address are not mutated at all + AbiBool _ -> genAbiValue AbiBoolType -- Booleans are regenerated AbiBytes n b -> do fs <- replicateM n getRandom xs <- mutateLL (Just n) (BS.pack fs) b pure $ AbiBytes n xs @@ -327,6 +336,7 @@ mutateAbiValue = \case AbiFunction v -> pure $ AbiFunction v -- | Given a 'SolCall', generate a random \"similar\" call with the same 'SolSignature'. +-- Note that this funcion will mutate a *single* argument (if any) mutateAbiCall :: MonadRandom m => SolCall -> m SolCall mutateAbiCall = traverse f where f [] = pure [] @@ -354,6 +364,7 @@ genWithDict genDict m g t = do Just cs -> Just <$> rElem' cs fromMaybe <$> g t <*> maybeValM +-- | A small number of dummy addresses pregenAdds :: [Addr] pregenAdds = [i*0xffffffff | i <- [1 .. 3]] @@ -361,6 +372,7 @@ pregenAbiAdds :: [AbiValue] pregenAbiAdds = map (AbiAddress . fromIntegral) pregenAdds -- | Synthesize a random 'AbiValue' given its 'AbiType'. Requires a dictionary. +-- Only produce lists with number of elements in the range [1, 32] genAbiValueM :: MonadRandom m => GenDict -> AbiType -> m AbiValue genAbiValueM genDict = genWithDict genDict genDict.constants $ \case AbiUIntType n -> fixAbiUInt n . fromInteger <$> getRandomUint n diff --git a/lib/Echidna/Shrink.hs b/lib/Echidna/Shrink.hs index a8641d5a1..b381ec096 100644 --- a/lib/Echidna/Shrink.hs +++ b/lib/Echidna/Shrink.hs @@ -21,6 +21,7 @@ import Echidna.Types.Config import Echidna.Types.Campaign (CampaignConf(..)) import Echidna.Test (getResultFromVM, checkETest) + -- | Top level function to shrink the complexity of the sequence of transactions once shrinkTest :: (MonadIO m, MonadThrow m, MonadRandom m, MonadReader Env m) => VM Concrete RealWorld @@ -29,14 +30,20 @@ shrinkTest shrinkTest vm test = do env <- ask case test.state of + -- If we run out of tries to shrink, return the sequence as we have them Large i | i >= env.cfg.campaignConf.shrinkLimit && not (isOptimizationTest test) -> pure $ Just test { state = Solved } Large i -> + -- Start removing the reverts, if any do repro <- removeReverts vm test.reproducer - let rr = removeUselessNoCalls $ catNoCalls repro + let rr = removeUselessNoCalls $ catNoCalls repro + -- Check if the sequence can be reduced, in practice this is almost never fails + -- since the canShrinkTx function is hard to enforce for all transaction in the sequence if length rr > 1 || any canShrinkTx rr then do maybeShrunk <- shrinkSeq vm (checkETest test) test.value rr + -- check if the shrinked sequence passes the test or not pure $ case maybeShrunk of + -- the test still fails, let's create another test with the reduced sequence Just (txs, val, vm') -> do Just test { state = Large (i + 1) , reproducer = txs @@ -44,7 +51,7 @@ shrinkTest vm test = do , result = getResultFromVM vm' , value = val } Nothing -> - -- No success with shrinking this time, just bump trials + -- The test passed, so no success with shrinking this time, just bump number of tries to shrink Just test { state = Large (i + 1), reproducer = rr} else pure $ Just test { state = if isOptimizationTest test @@ -55,10 +62,15 @@ shrinkTest vm test = do replaceByNoCall :: Tx -> Tx replaceByNoCall tx = tx { call = NoCall } +-- | Given a sequence of transactions, remove useless NoCalls. These +-- are when the NoCall have both zero increment in timestamp and block number. removeUselessNoCalls :: [Tx] -> [Tx] removeUselessNoCalls = mapMaybe f where f tx = if isUselessNoCall tx then Nothing else Just tx +-- | Given a VM and a sequence of transactions, execute each transaction except the last one. +-- If a transaction reverts, replace it by a "NoCall" with the same parameters as the original call +-- (e.g. same block increment timestamp and number) removeReverts :: (MonadIO m, MonadReader Env m, MonadThrow m) => VM Concrete RealWorld -> [Tx] -> m [Tx] removeReverts vm txs = do let (itxs, le) = (init txs, last txs) @@ -83,10 +95,14 @@ shrinkSeq -> [Tx] -> m (Maybe ([Tx], TestValue, VM Concrete RealWorld)) shrinkSeq vm f v txs = do + -- apply one of the two possible simplification strategies (shrunk or shorten) with equal probability txs' <- uniform =<< sequence [shorten, shrunk] + -- remove certain type of "no calls" let txs'' = removeUselessNoCalls txs' + -- check if the sequence still triggers a failed transaction (value, vm') <- check txs'' vm - -- if the test passed it means we didn't shrink successfully + -- if the test passed it means we didn't shrink successfully (returns Nothing) + -- otherwise, return a reduced sequence of transaction pure $ case (value,v) of (BoolValue False, _) -> Just (txs'', value, vm') (IntValue x, IntValue y) | x >= y -> Just (txs'', value, vm') @@ -96,9 +112,15 @@ shrinkSeq vm f v txs = do check (x:xs') vm' = do (_, vm'') <- execTx vm' x check xs' vm'' + -- | Simplify a sequence of transactions reducing the complexity of its arguments (using shrinkTx) + -- and then reducing its sender (using shrinkSender) shrunk = mapM (shrinkSender <=< shrinkTx) txs + -- | Simplifiy a sequence of transactions randomly dropping one transaction (with uniform selection) shorten = (\i -> take i txs ++ drop (i + 1) txs) <$> getRandomR (0, length txs) +-- | Given a transaction, replace the sender of the transaction by another one +-- which is simpler (e.g. it is closer to zero). Usually this means that +-- simplified transactions will try to use 0x10000 as the same caller shrinkSender :: (MonadReader Env m, MonadRandom m) => Tx -> m Tx shrinkSender x = do senderSet <- asks (.cfg.solConf.sender)