Skip to content

Commit e09cee2

Browse files
authored
Merge pull request #1392 from crytic/gas-per-second-text
Show gas/s metric in non-interactive mode
2 parents 3797538 + 1d313b4 commit e09cee2

File tree

8 files changed

+82
-22
lines changed

8 files changed

+82
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
* feat: show which project is being fuzzed (#1381)
44
* feat: keyboard navigation for the UI (Tab, PgUp, PgDown, arrows) (#1386)
5+
* feat: gas/s reporting on text mode (#1392)
56
* ARM64 Docker containers (#1352)
67
* ARM64 Linux builds (#1377)
78
* Fix worker crashes when shrinking empty reproducers (#1378)
89
* Fix shrinking sometimes not progressing (#1399)
10+
* Fix gas accounting; it was not considering the intrinsic cost of transactions (#1392)
911
* Fix issue collecting deployed contract addresses into the dictionary (#1400)
1012
* Improved UI responsiveness (#1387)
1113
* Update `hevm` to reduce memory usage on certain scenarios (#1346)

lib/Echidna/Transaction.hs

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,29 @@ module Echidna.Transaction where
66
import Optics.Core
77
import Optics.State.Operators
88

9-
import Control.Monad (join)
9+
import Control.Monad (join, when)
1010
import Control.Monad.IO.Class (MonadIO, liftIO)
1111
import Control.Monad.Random.Strict (MonadRandom, getRandomR, uniform)
1212
import Control.Monad.Reader (MonadReader, ask)
1313
import Control.Monad.State.Strict (MonadState, gets, modify', execState)
1414
import Control.Monad.ST (RealWorld)
15+
import Data.ByteString qualified as BS
1516
import Data.Map (Map, toList)
1617
import Data.Maybe (catMaybes)
1718
import Data.Set (Set)
1819
import Data.Set qualified as Set
1920
import Data.Vector qualified as V
2021

21-
import EVM (initialContract, loadContract, resetState)
22+
import EVM (ceilDiv, initialContract, loadContract, resetState)
2223
import EVM.ABI (abiValueType)
23-
import EVM.Types hiding (Env, VMOpts(timestamp, gasprice))
24+
import EVM.FeeSchedule (FeeSchedule(..))
25+
import EVM.Types hiding (Env, Gas, VMOpts(timestamp, gasprice))
2426

2527
import Echidna.ABI
2628
import Echidna.Orphans.JSON ()
2729
import Echidna.SourceMapping (lookupUsingCodehash)
2830
import Echidna.Symbolic (forceWord, forceAddr)
29-
import Echidna.Types (fromEVM)
31+
import Echidna.Types (fromEVM, Gas)
3032
import Echidna.Types.Config (Env(..), EConfig(..))
3133
import Echidna.Types.Random
3234
import Echidna.Types.Signature
@@ -177,25 +179,49 @@ setupTx tx@Tx{call} = fromEVM $ do
177179
, block = advanceBlock vm.block tx.delay
178180
, tx = vm.tx { gasprice = tx.gasprice, origin = LitAddr tx.src }
179181
}
180-
case call of
181-
SolCreate bc -> do
182-
#env % #contracts % at (LitAddr tx.dst) .=
183-
Just (initialContract (InitCode bc mempty) & set #balance (Lit tx.value))
184-
modify' $ execState $ loadContract (LitAddr tx.dst)
185-
#state % #code .= RuntimeCode (ConcreteRuntimeCode bc)
186-
SolCall cd -> do
187-
incrementBalance
188-
modify' $ execState $ loadContract (LitAddr tx.dst)
189-
#state % #calldata .= ConcreteBuf (encode cd)
190-
SolCalldata cd -> do
191-
incrementBalance
192-
modify' $ execState $ loadContract (LitAddr tx.dst)
193-
#state % #calldata .= ConcreteBuf cd
182+
when isCreate $ do
183+
#env % #contracts % at (LitAddr tx.dst) .=
184+
Just (initialContract (InitCode calldata mempty) & set #balance (Lit tx.value))
185+
modify' $ execState $ loadContract (LitAddr tx.dst)
186+
#state % #code .= RuntimeCode (ConcreteRuntimeCode calldata)
187+
when isCall $ do
188+
incrementBalance
189+
modify' $ execState $ loadContract (LitAddr tx.dst)
190+
#state % #calldata .= ConcreteBuf calldata
191+
modify' $ \vm ->
192+
let intrinsicGas = txGasCost vm.block.schedule isCreate calldata
193+
burned = min intrinsicGas vm.state.gas
194+
in vm & #state % #gas %!~ subtract burned
195+
& #burned %!~ (+ burned)
194196
where
195197
incrementBalance = #env % #contracts % ix (LitAddr tx.dst) % #balance %= (\v -> Lit $ forceWord v + tx.value)
196198
encode (n, vs) = abiCalldata (encodeSig (n, abiValueType <$> vs)) $ V.fromList vs
199+
isCall = case call of
200+
SolCall _ -> True
201+
SolCalldata _ -> True
202+
_ -> False
203+
isCreate = case call of
204+
SolCreate _ -> True
205+
_ -> False
206+
calldata = case call of
207+
SolCreate bc -> bc
208+
SolCall cd -> encode cd
209+
SolCalldata cd -> cd
197210

198211
advanceBlock :: Block -> (W256, W256) -> Block
199212
advanceBlock blk (t,b) =
200213
blk { timestamp = Lit (forceWord blk.timestamp + t)
201214
, number = Lit (forceWord blk.number + b) }
215+
216+
-- | Calculate transaction gas cost for Echidna Tx
217+
-- Adapted from HEVM's txGasCost function
218+
txGasCost :: FeeSchedule Gas -> Bool -> BS.ByteString -> Gas
219+
txGasCost fs isCreate calldata = baseCost + zeroCost + nonZeroCost
220+
where
221+
zeroBytes = BS.count 0 calldata
222+
nonZeroBytes = BS.length calldata - zeroBytes
223+
baseCost = fs.g_transaction
224+
+ (if isCreate then fs.g_txcreate + initcodeCost else 0)
225+
zeroCost = fs.g_txdatazero * fromIntegral zeroBytes
226+
nonZeroCost = fs.g_txdatanonzero * fromIntegral nonZeroBytes
227+
initcodeCost = fs.g_initcodeword * fromIntegral (ceilDiv (BS.length calldata) 32)

lib/Echidna/UI.hs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ data UIEvent =
5353
(Map Addr (Map W256 (Maybe W256)))
5454
| EventReceived (LocalTime, CampaignEvent)
5555

56+
-- | Gas tracking state for calculating gas consumption rate
57+
data GasTracker = GasTracker
58+
{ lastUpdateTime :: LocalTime
59+
, totalGasConsumed :: Int
60+
}
61+
5662
-- | Set up and run an Echidna 'Campaign' and display interactive UI or
5763
-- print non-interactive output in desired format at the end
5864
ui
@@ -174,10 +180,14 @@ ui vm dict initialCorpus cliSelectedContract = do
174180
let forwardEvent ev = putStrLn =<< runReaderT (ppLogLine vm ev) env
175181
uiEventsForwarderStopVar <- spawnListener forwardEvent
176182

183+
-- Track last update time and gas for delta calculation
184+
startTime <- liftIO getTimestamp
185+
lastUpdateRef <- liftIO $ newIORef $ GasTracker startTime 0
186+
177187
let printStatus = do
178188
states <- liftIO $ workerStates workers
179189
time <- timePrefix <$> getTimestamp
180-
line <- statusLine env states
190+
line <- statusLine env states lastUpdateRef
181191
putStrLn $ time <> "[status] " <> line
182192
hFlush stdout
183193

@@ -379,15 +389,27 @@ isTerminal = hNowSupportsANSI stdout
379389
statusLine
380390
:: Env
381391
-> [WorkerState]
392+
-> IORef GasTracker -- Gas consumption tracking state
382393
-> IO String
383-
statusLine env states = do
394+
statusLine env states lastUpdateRef = do
384395
tests <- traverse readIORef env.testRefs
385396
(points, _) <- coverageStats env.coverageRefInit env.coverageRefRuntime
386397
corpus <- readIORef env.corpusRef
398+
now <- getTimestamp
387399
let totalCalls = sum ((.ncalls) <$> states)
400+
let totalGas = sum ((.totalGas) <$> states)
401+
402+
-- Calculate delta-based gas/s
403+
gasTracker <- readIORef lastUpdateRef
404+
let deltaTime = round $ diffLocalTime now gasTracker.lastUpdateTime
405+
let deltaGas = totalGas - gasTracker.totalGasConsumed
406+
let gasPerSecond = if deltaTime > 0 then deltaGas `div` deltaTime else 0
407+
writeIORef lastUpdateRef $ GasTracker now totalGas
408+
388409
pure $ "tests: " <> show (length $ filter didFail tests) <> "/" <> show (length tests)
389410
<> ", fuzzing: " <> show totalCalls <> "/" <> show env.cfg.campaignConf.testLimit
390411
<> ", values: " <> show ((.value) <$> filter isOptimizationTest tests)
391412
<> ", cov: " <> show points
392413
<> ", corpus: " <> show (Corpus.corpusSize corpus)
414+
<> ", gas/s: " <> show gasPerSecond
393415

src/test/Tests/Integration.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ integrationTests = testGroup "Solidity Integration Testing"
9292
]
9393
, testContract "basic/gaslimit.sol" Nothing
9494
[ ("echidna_gaslimit passed", passed "echidna_gaslimit") ]
95+
, testContract "basic/gasleft.sol" (Just "basic/gasleft.yaml")
96+
[ ("unexpected gas left", passed "echidna_expected_gasleft") ]
9597
, testContractV "basic/killed.sol" (Just (< solcV (0,8,0))) (Just "basic/killed.yaml")
9698
[ ("echidna_still_alive failed", solved "echidna_still_alive") ]
9799
, checkConstructorConditions "basic/codesize.sol"

tests/solidity/basic/gasleft.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
contract C {
2+
function echidna_expected_gasleft() public returns (bool) {
3+
uint256 left = gasleft();
4+
return left > 2000 && left < 5000;
5+
}
6+
}

tests/solidity/basic/gasleft.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testLimit: 10
2+
propMaxGas: 25000

tests/solidity/basic/gasuse.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
testLimit: 5000
1+
testLimit: 10000
22
testMaxGas: 4294967295
33
estimateGas: true
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
propMaxGas: 20000
1+
propMaxGas: 40000

0 commit comments

Comments
 (0)