From d87e720342bf48bcf5cb10a77c848f5fe109074e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <31542053+gustavo-grieco@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:54:20 -0300 Subject: [PATCH] Small standalone fixes: coverage attribution, generic log events (#1599) * feat: name the function that found new coverage ppWorkerEvent already receives the transaction sequence behind a NewCoverage event but only reported the aggregate counters. Append the last transaction's call to the log line so it is visible which function is making progress. Co-authored-by: gustavo-grieco * feat: add generic log events Add a WorkerEvent Log constructor for free-form worker messages and a CampaignEvent ServerLog for messages that are not attributable to any worker, reported with a [Server] prefix in the text UI and as a server_log SSE event. All four exhaustive matches over these types (the ToJSON instances, the pretty printers and the SSE event names) land together so the compiler stays happy. Co-authored-by: gustavo-grieco * fix(tests): return the flag from flags.sol set0/set1 Both functions are declared 'returns (bool)' but fall off the end, so solc warns and they silently return false. Co-authored-by: gustavo-grieco * chore: gitignore Python artifacts Co-authored-by: gustavo-grieco * fix(worker): avoid hlint false positive on record-dot access CI runs bare `hlint lib src`, which does not read echidna.cabal and so does not know OverloadedRecordDot is on. It parsed `(last txs).call` as function composition and flagged the parens as a redundant bracket. Taking that suggestion would have broken the build: under OverloadedRecordDot, `last txs.call` parses as `last (txs.call)`. Bind the last transaction first so the expression needs no parens and reads identically to hlint and to GHC. --------- Co-authored-by: gustavo-grieco --- .gitignore | 6 ++++++ lib/Echidna/Server.hs | 6 ++++++ lib/Echidna/Types/Worker.hs | 2 ++ lib/Echidna/UI/Report.hs | 2 ++ lib/Echidna/Worker.hs | 19 ++++++++++++++++--- tests/solidity/basic/flags.sol | 6 ++++-- 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e3cbf16d..f381e45a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ crytic-export/ echidna.cabal *.swp profiles/ + +# Python helper scripts and test suites +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ diff --git a/lib/Echidna/Server.hs b/lib/Echidna/Server.hs index 60cf57d7..f67658bb 100644 --- a/lib/Echidna/Server.hs +++ b/lib/Echidna/Server.hs @@ -36,6 +36,10 @@ instance ToJSON SSE where object [ "timestamp" .= time , "filename" .= filename ] + toJSON (SSE (time, ServerLog msg)) = + object [ "timestamp" .= time + , "data" .= msg + ] runSSEServer :: MVar () -> Env -> Word16 -> Int -> IO () runSSEServer serverStopVar env port nworkers = do @@ -53,11 +57,13 @@ runSSEServer serverStopVar env port nworkers = do NewCoverage {} -> "new_coverage" SymExecLog _ -> "sym_exec_log" SymExecError _ -> "sym_exec_error" + Log _ -> "log" TxSequenceReplayed {} -> "tx_sequence_replayed" TxSequenceReplayFailed {} -> "tx_sequence_replay_failed" WorkerStopped _ -> "worker_stopped" Failure _err -> "failure" ReproducerSaved _ -> "saved_reproducer" + ServerLog _ -> "server_log" let serverEvent = ServerEvent { eventName = Just (eventName campaignEvent) diff --git a/lib/Echidna/Types/Worker.hs b/lib/Echidna/Types/Worker.hs index be56a7c6..66011fd9 100644 --- a/lib/Echidna/Types/Worker.hs +++ b/lib/Echidna/Types/Worker.hs @@ -11,6 +11,7 @@ data CampaignEvent = WorkerEvent WorkerId WorkerType WorkerEvent | Failure String | ReproducerSaved String -- filename + | ServerLog String data WorkerEvent = TestFalsified !EchidnaTest @@ -18,6 +19,7 @@ data WorkerEvent | NewCoverage { points :: !Int, numCodehashes :: !Int, corpusSize :: !Int, transactions :: [Tx] } | SymExecError !String | SymExecLog !String + | Log !String | TxSequenceReplayed FilePath !Int !Int | TxSequenceReplayFailed FilePath Tx | WorkerStopped WorkerStopReason diff --git a/lib/Echidna/UI/Report.hs b/lib/Echidna/UI/Report.hs index 87adc1c5..14401071 100644 --- a/lib/Echidna/UI/Report.hs +++ b/lib/Echidna/UI/Report.hs @@ -34,6 +34,8 @@ ppLogLine vm (time, event@(WorkerEvent workerId FuzzWorker _)) = ((timePrefix time <> "[Worker " <> show workerId <> "] ") <>) <$> ppCampaignEventLog vm event ppLogLine vm (time, event@(WorkerEvent workerId SymbolicWorker _)) = ((timePrefix time <> "[Worker " <> show workerId <> ", symbolic] ") <>) <$> ppCampaignEventLog vm event +ppLogLine vm (time, event@(ServerLog _)) = + ((timePrefix time <> "[Server] ") <>) <$> ppCampaignEventLog vm event ppLogLine vm (time, event) = ((timePrefix time <> " ") <>) <$> ppCampaignEventLog vm event diff --git a/lib/Echidna/Worker.hs b/lib/Echidna/Worker.hs index b8322969..7f47fdef 100644 --- a/lib/Echidna/Worker.hs +++ b/lib/Echidna/Worker.hs @@ -12,6 +12,7 @@ import Echidna.ABI (encodeSig) import Echidna.Types.Campaign import Echidna.Types.Config (Env(..), EConfig(..)) import Echidna.Types.Test +import Echidna.Types.Tx (Tx(..), TxCall(..)) import Echidna.Types.Worker import Echidna.Utility (getTimestamp) @@ -30,6 +31,7 @@ instance ToJSON WorkerEvent where object [ "coverage" .= points, "contracts" .= numCodehashes, "corpus_size" .= corpusSize] SymExecError msg -> object [ "msg" .= msg ] SymExecLog msg -> object [ "msg" .= msg ] + Log msg -> object [ "msg" .= msg ] TxSequenceReplayed file current total -> object [ "file" .= file, "current" .= current, "total" .= total ] TxSequenceReplayFailed file tx -> @@ -97,6 +99,7 @@ ppCampaignEvent = \case WorkerEvent _ _ e -> ppWorkerEvent e Failure err -> err ReproducerSaved f -> "Saved reproducer to " <> f + ServerLog msg -> msg ppWorkerEvent :: WorkerEvent -> String ppWorkerEvent = \case @@ -105,14 +108,24 @@ ppWorkerEvent = \case TestOptimized test -> let name = case test.testType of OptimizationTest n _ -> n; _ -> error "fixme" in "New maximum value of " <> unpack name <> ": " <> show test.value - NewCoverage { points, numCodehashes, corpusSize } -> - "New coverage: " <> show points <> " instr, " + NewCoverage { points, numCodehashes, corpusSize, transactions } -> + let -- the coverage is credited to the last transaction of the sequence + culprit = case transactions of + [] -> "init" + txs -> let tx = last txs in case tx.call of + SolCall (name, _) -> unpack name + SolCreate _ -> "constructor" + SolCalldata _ -> "fallback" + NoCall -> "no call" + in "New coverage: " <> show points <> " instr, " <> show numCodehashes <> " contracts, " - <> show corpusSize <> " seqs in corpus" + <> show corpusSize <> " seqs in corpus (" <> culprit <> ")" SymExecError err -> "Symbolic execution failed: " <> err SymExecLog msg -> "Symbolic execution log: " <> msg + Log msg -> + msg TxSequenceReplayed file current total -> "Sequence replayed from corpus file " <> file <> " (" <> show current <> "/" <> show total <> ")" TxSequenceReplayFailed file tx -> diff --git a/tests/solidity/basic/flags.sol b/tests/solidity/basic/flags.sol index 82c66a20..c3845c4f 100644 --- a/tests/solidity/basic/flags.sol +++ b/tests/solidity/basic/flags.sol @@ -5,13 +5,15 @@ contract Test { bool private flag1 = true; function set0(int val) public returns (bool){ - if (val % 100 == 0) + if (val % 100 == 0) flag0 = false; + return flag0; } function set1(int val) public returns (bool){ - if (val % 10 == 0 && !flag0) + if (val % 10 == 0 && !flag0) flag1 = false; + return flag1; } function echidna_alwaystrue() public returns (bool){