Skip to content

Commit 6956030

Browse files
Add missing documentation (#1298)
* initial documentation of shrink * expanded documentation of generation and mutation
1 parent 182580e commit 6956030

File tree

2 files changed

+41
-7
lines changed

2 files changed

+41
-7
lines changed

lib/Echidna/ABI.hs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,20 @@ mkDictValues =
153153
fromValue (AbiInt _ n) = Just (fromIntegral n)
154154
fromValue _ = Nothing
155155

156-
-- Generation (synthesis)
157-
156+
-- Generate a random integer using a pow scale:
158157
getRandomPow :: (MonadRandom m) => Int -> m Integer
159158
getRandomPow n = if n <= 0 then return 0 else
160159
do
160+
-- generate uniformly a number from 20 to n
161161
mexp <- getRandomR (20, n)
162+
-- generate uniformly a number from the range 2 ^ (mexp / 2) to 2 ^ mexp
162163
getRandomR (2 ^ (mexp `div` 2), 2 ^ mexp)
163164

165+
-- Generate a random unsigned integer with the following distribution:
166+
-- * 9% (2/21) uniformly from 0 to 1024
167+
-- * 76% (16/21) uniformly from 0 to 2 ^ n - 5
168+
-- * 9% (2/21) uniformly from 2 ^ n - 5 to 2 ^ n - 1.
169+
-- * 4% (1/21) using the getRandomPow function
164170
getRandomUint :: MonadRandom m => Int -> m Integer
165171
getRandomUint n =
166172
join $ Random.weighted
@@ -170,6 +176,9 @@ getRandomUint n =
170176
, (getRandomPow (n - 5), 1)
171177
]
172178

179+
-- | Generate a random signed integer with the following distribution:
180+
-- * 10% uniformly from the range -1023 to 1023.
181+
-- * 90% uniformly from the range -1 * 2 ^ n to 2 ^ (n - 1).
173182
getRandomInt :: MonadRandom m => Int -> m Integer
174183
getRandomInt n =
175184
getRandomR =<< Random.weighted
@@ -310,8 +319,8 @@ mutateAbiValue = \case
310319
\case 0 -> fixAbiInt n <$> mutateNum x
311320
_ -> pure $ AbiInt n x
312321

313-
AbiAddress x -> pure $ AbiAddress x
314-
AbiBool _ -> genAbiValue AbiBoolType
322+
AbiAddress x -> pure $ AbiAddress x -- Address are not mutated at all
323+
AbiBool _ -> genAbiValue AbiBoolType -- Booleans are regenerated
315324
AbiBytes n b -> do fs <- replicateM n getRandom
316325
xs <- mutateLL (Just n) (BS.pack fs) b
317326
pure $ AbiBytes n xs
@@ -327,6 +336,7 @@ mutateAbiValue = \case
327336
AbiFunction v -> pure $ AbiFunction v
328337

329338
-- | Given a 'SolCall', generate a random \"similar\" call with the same 'SolSignature'.
339+
-- Note that this funcion will mutate a *single* argument (if any)
330340
mutateAbiCall :: MonadRandom m => SolCall -> m SolCall
331341
mutateAbiCall = traverse f
332342
where f [] = pure []
@@ -354,13 +364,15 @@ genWithDict genDict m g t = do
354364
Just cs -> Just <$> rElem' cs
355365
fromMaybe <$> g t <*> maybeValM
356366

367+
-- | A small number of dummy addresses
357368
pregenAdds :: [Addr]
358369
pregenAdds = [i*0xffffffff | i <- [1 .. 3]]
359370

360371
pregenAbiAdds :: [AbiValue]
361372
pregenAbiAdds = map (AbiAddress . fromIntegral) pregenAdds
362373

363374
-- | Synthesize a random 'AbiValue' given its 'AbiType'. Requires a dictionary.
375+
-- Only produce lists with number of elements in the range [1, 32]
364376
genAbiValueM :: MonadRandom m => GenDict -> AbiType -> m AbiValue
365377
genAbiValueM genDict = genWithDict genDict genDict.constants $ \case
366378
AbiUIntType n -> fixAbiUInt n . fromInteger <$> getRandomUint n

lib/Echidna/Shrink.hs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Echidna.Types.Config
2121
import Echidna.Types.Campaign (CampaignConf(..))
2222
import Echidna.Test (getResultFromVM, checkETest)
2323

24+
-- | Top level function to shrink the complexity of the sequence of transactions once
2425
shrinkTest
2526
:: (MonadIO m, MonadThrow m, MonadRandom m, MonadReader Env m)
2627
=> VM Concrete RealWorld
@@ -29,22 +30,28 @@ shrinkTest
2930
shrinkTest vm test = do
3031
env <- ask
3132
case test.state of
33+
-- If we run out of tries to shrink, return the sequence as we have them
3234
Large i | i >= env.cfg.campaignConf.shrinkLimit && not (isOptimizationTest test) ->
3335
pure $ Just test { state = Solved }
3436
Large i ->
37+
-- Start removing the reverts, if any
3538
do repro <- removeReverts vm test.reproducer
36-
let rr = removeUselessNoCalls $ catNoCalls repro
39+
let rr = removeUselessNoCalls $ catNoCalls repro
40+
-- Check if the sequence can be reduced, in practice this is almost never fails
41+
-- since the canShrinkTx function is hard to enforce for all transaction in the sequence
3742
if length rr > 1 || any canShrinkTx rr then do
3843
maybeShrunk <- shrinkSeq vm (checkETest test) test.value rr
44+
-- check if the shrinked sequence passes the test or not
3945
pure $ case maybeShrunk of
46+
-- the test still fails, let's create another test with the reduced sequence
4047
Just (txs, val, vm') -> do
4148
Just test { state = Large (i + 1)
4249
, reproducer = txs
4350
, vm = Just vm'
4451
, result = getResultFromVM vm'
4552
, value = val }
4653
Nothing ->
47-
-- No success with shrinking this time, just bump trials
54+
-- The test passed, so no success with shrinking this time, just bump number of tries to shrink
4855
Just test { state = Large (i + 1), reproducer = rr}
4956
else
5057
pure $ Just test { state = if isOptimizationTest test
@@ -55,10 +62,15 @@ shrinkTest vm test = do
5562
replaceByNoCall :: Tx -> Tx
5663
replaceByNoCall tx = tx { call = NoCall }
5764

65+
-- | Given a sequence of transactions, remove useless NoCalls. These
66+
-- are when the NoCall have both zero increment in timestamp and block number.
5867
removeUselessNoCalls :: [Tx] -> [Tx]
5968
removeUselessNoCalls = mapMaybe f
6069
where f tx = if isUselessNoCall tx then Nothing else Just tx
6170

71+
-- | Given a VM and a sequence of transactions, execute each transaction except the last one.
72+
-- If a transaction reverts, replace it by a "NoCall" with the same parameters as the original call
73+
-- (e.g. same block increment timestamp and number)
6274
removeReverts :: (MonadIO m, MonadReader Env m, MonadThrow m) => VM Concrete RealWorld -> [Tx] -> m [Tx]
6375
removeReverts vm txs = do
6476
let (itxs, le) = (init txs, last txs)
@@ -83,10 +95,14 @@ shrinkSeq
8395
-> [Tx]
8496
-> m (Maybe ([Tx], TestValue, VM Concrete RealWorld))
8597
shrinkSeq vm f v txs = do
98+
-- apply one of the two possible simplification strategies (shrunk or shorten) with equal probability
8699
txs' <- uniform =<< sequence [shorten, shrunk]
100+
-- remove certain type of "no calls"
87101
let txs'' = removeUselessNoCalls txs'
102+
-- check if the sequence still triggers a failed transaction
88103
(value, vm') <- check txs'' vm
89-
-- if the test passed it means we didn't shrink successfully
104+
-- if the test passed it means we didn't shrink successfully (returns Nothing)
105+
-- otherwise, return a reduced sequence of transaction
90106
pure $ case (value,v) of
91107
(BoolValue False, _) -> Just (txs'', value, vm')
92108
(IntValue x, IntValue y) | x >= y -> Just (txs'', value, vm')
@@ -96,9 +112,15 @@ shrinkSeq vm f v txs = do
96112
check (x:xs') vm' = do
97113
(_, vm'') <- execTx vm' x
98114
check xs' vm''
115+
-- | Simplify a sequence of transactions reducing the complexity of its arguments (using shrinkTx)
116+
-- and then reducing its sender (using shrinkSender)
99117
shrunk = mapM (shrinkSender <=< shrinkTx) txs
118+
-- | Simplifiy a sequence of transactions randomly dropping one transaction (with uniform selection)
100119
shorten = (\i -> take i txs ++ drop (i + 1) txs) <$> getRandomR (0, length txs)
101120

121+
-- | Given a transaction, replace the sender of the transaction by another one
122+
-- which is simpler (e.g. it is closer to zero). Usually this means that
123+
-- simplified transactions will try to use 0x10000 as the same caller
102124
shrinkSender :: (MonadReader Env m, MonadRandom m) => Tx -> m Tx
103125
shrinkSender x = do
104126
senderSet <- asks (.cfg.solConf.sender)

0 commit comments

Comments
 (0)