Skip to content

Commit e5f0023

Browse files
implemented status command
1 parent 0581d68 commit e5f0023

File tree

6 files changed

+90
-29
lines changed

6 files changed

+90
-29
lines changed

lib/Echidna/Agent/Fuzzer.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ fuzzerLoop callback vm testLimit bus = do
169169
| otherwise ->
170170
callback >> pure TestLimitReached
171171

172-
fuzz = randseq vm.env.contracts >>= fmap fst . callseq vm
172+
fuzz = randseq vm.env.contracts >>= fmap fst . (\txs -> callseq vm txs False)
173173

174174
shrink = do
175175
wid <- gets (.workerId)
@@ -214,7 +214,7 @@ fuzzerLoop callback vm testLimit bus = do
214214
Just (WrappedMessage _ (ToFuzzer tid (ExecuteSequence txs replyVar))) -> do
215215
workerId <- gets (.workerId)
216216
when (tid == workerId) $ do
217-
(_, newCov) <- callseq vm txs
217+
(_, newCov) <- callseq vm txs False
218218
liftIO $ case replyVar of
219219
Just var -> atomically $ putTMVar var newCov
220220
Nothing -> pure ()

lib/Echidna/Agent/Symbolic.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ handleMessage
137137
-> VM Concrete
138138
-> Maybe Text
139139
-> m ()
140-
handleMessage _ (WrappedMessage _ (Broadcast (NewCoverageInfo _ txs))) callback vm name = do
141-
void $ callseq vm txs
140+
handleMessage _ (WrappedMessage _ (Broadcast (NewCoverageInfo _ txs _))) callback vm name = do
141+
void $ callseq vm txs False
142142
symexecTxs callback vm False name txs
143143
shrinkAndRandomlyExplore callback vm txs (10 :: Int)
144144

@@ -366,7 +366,7 @@ exploreAndVerify callback vm contract method vm' txsBase = do
366366
-- For now, let's assume I can get it.
367367
-- I'll pass it from runAgent -> busListenerLoop -> handleMessage -> symexecTxs -> symexecTx -> exploreAndVerify
368368

369-
newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm (txsBase <> [symTx])) txs
369+
newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm (txsBase <> [symTx]) False) txs
370370

371371
when (not newCoverage && null errors && not (null txs)) (
372372
pushWorkerEvent $ SymExecError "No errors but symbolic execution found valid txs breaking assertions. Something is wrong.")
@@ -413,7 +413,7 @@ symExecMethod vm name callback contract method = do
413413
modify' (\ws -> ws { runningThreads = [] })
414414
callback
415415

416-
newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm [symTx]) txs
416+
newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm [symTx] False) txs
417417
let methodSignature = unpack method.methodSignature
418418
unless newCoverage $ do
419419
unless (null txs) $ error "No new coverage but symbolic execution found valid txs. Something is wrong."

lib/Echidna/Execution.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ replayCorpus vm txSeqs =
5959
List.filter (\case Tx { call = NoCall } -> False; _ -> True) txSeq
6060
case maybeFaultyTx of
6161
Nothing -> do
62-
_ <- callseq vm txSeq
62+
_ <- callseq vm txSeq True
6363
pushWorkerEvent (TxSequenceReplayed file i (length txSeqs))
6464
Just faultyTx ->
6565
pushWorkerEvent (TxSequenceReplayFailed file faultyTx)
@@ -71,8 +71,9 @@ callseq
7171
:: (MonadIO m, MonadThrow m, MonadRandom m, MonadReader Env m, MonadState WorkerState m)
7272
=> VM Concrete
7373
-> [Tx]
74+
-> Bool
7475
-> m (VM Concrete, Bool)
75-
callseq vm txSeq = do
76+
callseq vm txSeq isReplaying = do
7677
env <- ask
7778
-- First, we figure out whether we need to execute with or without coverage
7879
-- optimization and gas info, and pick our execution function appropriately
@@ -105,7 +106,7 @@ callseq vm txSeq = do
105106

106107
-- Broadcast new coverage to other agents (e.g. Symbolic)
107108
workerId <- gets (.workerId)
108-
liftIO $ atomically $ writeTChan env.bus (WrappedMessage (FuzzerId workerId) (Broadcast (NewCoverageInfo points (fst <$> results))))
109+
liftIO $ atomically $ writeTChan env.bus (WrappedMessage (FuzzerId workerId) (Broadcast (NewCoverageInfo points (fst <$> results) isReplaying)))
109110

110111
modify' $ \workerState ->
111112

lib/Echidna/MCP.hs

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33

44
module Echidna.MCP where
55

6+
import Control.Concurrent (forkIO)
7+
import Control.Monad (forever, when)
68
import Control.Concurrent.STM
7-
import Data.IORef (readIORef, IORef)
9+
import Data.IORef (readIORef, writeIORef, modifyIORef', newIORef, IORef)
810
import Data.List (find, isPrefixOf)
911
import qualified Data.Maybe
1012
import qualified Data.Set as Set
1113
import Data.Text (Text, pack, unpack)
1214
import qualified Data.Text as T
15+
import Data.Time (UTCTime, getCurrentTime, diffUTCTime)
1316
import Text.Printf (printf)
1417
import qualified Data.Map as Map
1518
import Text.Read (readMaybe)
@@ -22,14 +25,20 @@ import EVM.Solidity (SolcContract(..))
2225
import EVM.Types (Addr)
2326
import EVM.ABI (AbiValue(..))
2427
import Echidna.Types.Tx (Tx(..), TxCall(..))
25-
import Echidna.Types.Coverage (CoverageFileType(..), mergeCoverageMaps)
28+
import Echidna.Types.Coverage (CoverageFileType(..), mergeCoverageMaps, coverageStats)
2629
import Echidna.Output.Source (ppCoveredCode, saveLcovHook)
2730

2831
import Echidna.Types.Config (Env(..), EConfig(..))
29-
import Echidna.Types.Campaign (getNFuzzWorkers, CampaignConf(..))
30-
import Echidna.Types.InterWorker (Bus, Message(..), WrappedMessage(..), AgentId(..), FuzzerCmd(..))
32+
import Echidna.Types.Campaign (getNFuzzWorkers, CampaignConf(..), WorkerState(..))
33+
import Echidna.Types.InterWorker (Bus, Message(..), WrappedMessage(..), AgentId(..), FuzzerCmd(..), BroadcastMsg(..))
3134
import Echidna.Pretty (ppTx)
3235

36+
-- | Status state to track coverage info
37+
data StatusState = StatusState
38+
{ lastCoverageTime :: Maybe UTCTime
39+
, coveredFunctions :: [Text]
40+
}
41+
3342
-- | MCP Tool Definition
3443
-- Simulates the definition of a tool exposed by an MCP server.
3544
type ToolExecution = [(Text, Text)] -> Env -> Bus -> IORef [Text] -> IO String
@@ -40,11 +49,37 @@ data Tool = Tool
4049
, execute :: ToolExecution
4150
}
4251

43-
-- | Implementation of read_corpus tool
44-
readCorpusTool :: ToolExecution
45-
readCorpusTool _ env _ _ = do
52+
-- | Helper to get function name from Tx
53+
getFunctionName :: Tx -> Text
54+
getFunctionName tx = case tx.call of
55+
SolCall (name, _) -> name
56+
_ -> "unknown"
57+
58+
-- | Implementation of status tool
59+
statusTool :: [IORef WorkerState] -> IORef StatusState -> ToolExecution
60+
statusTool workerRefs statusRef _ env _ _ = do
4661
c <- readIORef env.corpusRef
47-
return $ printf "Corpus Size: %d" (Set.size c)
62+
st <- readIORef statusRef
63+
now <- getCurrentTime
64+
65+
-- Iterations
66+
workers <- mapM readIORef workerRefs
67+
let iterations = sum $ map (.ncalls) workers
68+
let maxIterations = env.cfg.campaignConf.testLimit
69+
70+
-- Coverage
71+
(covPoints, _) <- coverageStats env.coverageRefInit env.coverageRefRuntime
72+
73+
let timeStr = case st.lastCoverageTime of
74+
Nothing -> "Never"
75+
Just t -> show (round $ diffUTCTime now t)
76+
77+
funcs = if null st.coveredFunctions
78+
then "None"
79+
else unpack $ T.intercalate "\n- " st.coveredFunctions
80+
81+
return $ printf "Corpus Size: %d\nIterations: %d/%d\nCoverage: %d\nTime since last coverage: %s\nLast 10 covered functions:\n- %s"
82+
(Set.size c) iterations maxIterations covPoints timeStr funcs
4883

4984
-- | Implementation of inspect_corpus_transactions tool
5085
inspectCorpusTransactionsTool :: ToolExecution
@@ -252,21 +287,42 @@ showCoverageTool args env _ _ = do
252287
candidates -> return $ printf "Error: Ambiguous contract name '%s'. Found: %s" (unpack contractName) (unpack $ T.intercalate ", " $ map fst candidates)
253288

254289
-- | Registry of available tools
255-
availableTools :: [Tool]
256-
availableTools =
257-
[ Tool "read_corpus" "Read the current corpus size" readCorpusTool
290+
availableTools :: [IORef WorkerState] -> IORef StatusState -> [Tool]
291+
availableTools workerRefs statusRef =
292+
[ Tool "status" "Show fuzzing campaign status" (statusTool workerRefs statusRef)
258293
, Tool "inspect_corpus_transactions" "Browse the corpus transactions" inspectCorpusTransactionsTool
259294
, Tool "inject_transaction" "Inject a transaction into a sequence and execute it" injectTransactionTool
260295
, Tool "dump_lcov" "Dump coverage in LCOV format" dumpLcovTool
261296
, Tool "fuzz_transaction" "Fuzz a transaction with optional concrete arguments" fuzzTransactionTool
262297
, Tool "clear_priorities" "Clear the function prioritization list" clearPrioritiesTool
263-
, Tool "read_logs" "Read the last 100 log messages" readLogsTool
298+
--, Tool "read_logs" "Read the last 100 log messages" readLogsTool
264299
, Tool "show_coverage" "Show coverage report for a particular contract" showCoverageTool
265300
]
266301

267302
-- | Run the MCP Server
268-
runMCPServer :: Env -> Int -> IORef [Text] -> IO ()
269-
runMCPServer env port logsRef = do
303+
runMCPServer :: Env -> [IORef WorkerState] -> Int -> IORef [Text] -> IO ()
304+
runMCPServer env workerRefs port logsRef = do
305+
statusRef <- newIORef (StatusState Nothing [])
306+
307+
-- Spawn listener for coverage events
308+
myBus <- atomically $ dupTChan env.bus
309+
_ <- forkIO $ forever $ do
310+
msg <- atomically $ readTChan myBus
311+
case msg of
312+
WrappedMessage _ (Broadcast (NewCoverageInfo _ txs isReplaying)) -> do
313+
when (not isReplaying) $ do
314+
now <- getCurrentTime
315+
let funcNames = map getFunctionName txs
316+
lastFunc = if null funcNames then "unknown" else last funcNames
317+
318+
modifyIORef' statusRef $ \st -> st
319+
{ lastCoverageTime = Just now
320+
, coveredFunctions = take 10 (lastFunc : st.coveredFunctions)
321+
}
322+
_ -> return ()
323+
324+
let toolsList = availableTools workerRefs statusRef
325+
270326
let httpConfig = HttpConfig
271327
{ httpPort = port
272328
, httpHost = "127.0.0.1"
@@ -277,7 +333,7 @@ runMCPServer env port logsRef = do
277333
let serverInfo = McpServerInfo
278334
{ serverName = "Echidna MCP Server"
279335
, serverVersion = "1.0.0"
280-
, serverInstructions = "Echidna Agent Interface. Available tools: read_corpus, inspect_corpus_transactions, dump_lcov, fuzz_transaction, clear_priorities, read_logs, show_coverage"
336+
, serverInstructions = "Echidna Agent Interface. Available tools: status, inspect_corpus_transactions, dump_lcov, fuzz_transaction, clear_priorities, read_logs, show_coverage"
281337
}
282338

283339
let mkToolDefinition :: Tool -> ToolDefinition
@@ -317,6 +373,10 @@ runMCPServer env port logsRef = do
317373
{ properties = []
318374
, required = []
319375
}
376+
"status" -> InputSchemaDefinitionObject
377+
{ properties = []
378+
, required = []
379+
}
320380
_ -> InputSchemaDefinitionObject
321381
{ properties = []
322382
, required = []
@@ -325,11 +385,11 @@ runMCPServer env port logsRef = do
325385
, toolDefinitionMeta = Nothing
326386
}
327387

328-
let toolDefs = map mkToolDefinition availableTools
388+
let toolDefs = map mkToolDefinition toolsList
329389

330390
let handleToolCall :: ToolName -> [(ArgumentName, ArgumentValue)] -> IO (Either Error Content)
331391
handleToolCall name args = do
332-
case find (\t -> pack t.toolName == name) availableTools of
392+
case find (\t -> pack t.toolName == name) toolsList of
333393
Nothing -> return $ Left $ UnknownTool name
334394
Just tool -> do
335395
result <- tool.execute args env env.bus logsRef

lib/Echidna/Types/InterWorker.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ data Message
4242
deriving (Show)
4343

4444
data BroadcastMsg
45-
= NewCoverageInfo Int [Tx] -- points, transactions
45+
= NewCoverageInfo Int [Tx] Bool -- points, transactions, isReplaying
4646
| FoundBug EchidnaTest
4747
| StrategyUpdate Text
4848
| WorkerStopped AgentId

lib/Echidna/UI.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ ui vm dict initialCorpus cliSelectedContract = do
116116
case conf.campaignConf.serverPort of
117117
Just port -> do
118118
liftIO $ pushCampaignEvent env (ServerLog ("MCP Server running at http://127.0.0.1:" ++ show port ++ "/mcp"))
119-
void $ liftIO $ forkIO $ runMCPServer env (fromIntegral port) logBuffer
119+
void $ liftIO $ forkIO $ runMCPServer env (map snd workers) (fromIntegral port) logBuffer
120120
Nothing -> pure ()
121121

122122
ticker <- liftIO . forkIO . forever $ do
@@ -217,7 +217,7 @@ ui vm dict initialCorpus cliSelectedContract = do
217217
case conf.campaignConf.serverPort of
218218
Just port -> do
219219
liftIO $ pushCampaignEvent env (ServerLog ("MCP Server running at http://127.0.0.1:" ++ show port ++ "/mcp"))
220-
void $ liftIO $ forkIO $ runMCPServer env (fromIntegral port) logBuffer
220+
void $ liftIO $ forkIO $ runMCPServer env (map snd workers) (fromIntegral port) logBuffer
221221
Nothing -> pure ()
222222

223223
ticker <- liftIO . forkIO . forever $ do

0 commit comments

Comments
 (0)