Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/release-fork.yml
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file necessary?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file necessary?

No, I added it to generate builds on main

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think once it is here as a PR, you don't need this file and can be removed. Perhaps @elopez can confirm this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: "Build & Release"

on:
workflow_dispatch:
inputs:
version:
description: 'Version name (e.g., 2.3.0-custom)'
required: true
default: 'dev'
push:
branches:
- main
tags:
- "v*"

jobs:
build:
name: Build Linux (x86_64)
timeout-minutes: 30
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v21

- name: Configure Cachix
uses: cachix/cachix-action@v16
with:
name: trailofbits
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

- name: Set version
id: version
run: |
if [[ -n "${{ github.event.inputs.version }}" ]]; then
echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.ref }}" =~ ^refs/tags/v.* ]]; then
echo "version=$(echo "${{ github.ref }}" | sed 's#^refs/tags/v##')" >> "$GITHUB_OUTPUT"
else
echo "version=HEAD-$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT"
fi

- name: Build redistributable echidna
run: |
nix build ".#packages.x86_64-linux.echidna-redistributable" --out-link redistributable
tar -czf "echidna-${{ steps.version.outputs.version }}-x86_64-linux.tar.gz" -C ./redistributable/bin/ echidna

- name: Upload artifact
uses: actions/upload-artifact@v5
with:
name: echidna-x86_64-linux
path: echidna-${{ steps.version.outputs.version }}-x86_64-linux.tar.gz

release:
name: Create release
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v6
with:
pattern: echidna-*
merge-multiple: true

- name: List artifacts
run: ls -la echidna-*.tar.gz

- name: Create/Update rolling release (main branch)
if: github.ref == 'refs/heads/main'
uses: softprops/action-gh-release@v2
with:
tag_name: latest
name: "Echidna Latest (main)"
body: |
Rolling release from main branch.
Version: ${{ needs.build.outputs.version }}
Commit: ${{ github.sha }}
prerelease: true
files: ./echidna-*.tar.gz

- name: Create tagged release
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
draft: true
name: "Echidna ${{ needs.build.outputs.version }}"
tag_name: ${{ github.ref_name || format('v{0}', github.event.inputs.version) }}
files: ./echidna-*.tar.gz
3 changes: 2 additions & 1 deletion lib/Echidna.hs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ mkEnv cfg buildOutput tests world slitherInfo = do
contractNameCache <- newIORef mempty
-- TODO put in real path
let dapp = dappInfo "/" buildOutput
pure $ Env { cfg, dapp, codehashMap, fetchSession, contractNameCache
sourceCache = buildOutput.sources
pure $ Env { cfg, dapp, sourceCache, codehashMap, fetchSession, contractNameCache
, chainId, eventQueue, coverageRefInit, coverageRefRuntime, corpusRef, testRefs, world
, slitherInfo
}
24 changes: 23 additions & 1 deletion lib/Echidna/Output/Source.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE TemplateHaskell #-}

module Echidna.Output.Source where
module Echidna.Output.Source (
saveCoverages,
saveLcovHook,
checkAssertionsCoverage
) where

import Control.Monad (unless)
import Data.ByteString qualified as BS
Expand Down Expand Up @@ -90,6 +94,24 @@ coverageFileExtension Lcov = ".lcov"
coverageFileExtension Html = ".html"
coverageFileExtension Txt = ".txt"

-- | Save only LCOV coverage triggered by HTTP hook, with timestamp in filename
saveLcovHook
:: Env
-> FilePath
-> SourceCache
-> [SolcContract]
-> IO FilePath
saveLcovHook env d sc cs = do
coverage <- mergeCoverageMaps env.dapp env.coverageRefInit env.coverageRefRuntime
currentTime <- getCurrentTime
let timestamp = formatTime defaultTimeLocale "%Y%m%d_%H%M%S" currentTime
fn = d </> "hook_" <> timestamp <> ".lcov"
excludePatterns = env.cfg.campaignConf.coverageExcludes
cc = ppCoveredCode Lcov sc cs coverage Nothing (T.pack timestamp) excludePatterns
createDirectoryIfMissing True d
writeFile fn cc
pure fn

-- | Pretty-print the covered code
ppCoveredCode :: CoverageFileType -> SourceCache -> [SolcContract] -> FrozenCoverageMap -> Maybe Text -> Text -> [Text] -> Text
ppCoveredCode fileType sc cs s projectName timestamp excludePatterns
Expand Down
125 changes: 92 additions & 33 deletions lib/Echidna/Server.hs
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
module Echidna.Server where

import Control.Concurrent
import Control.Monad (when, void)
import Control.DeepSeq (force)
import Control.Monad (forever, when, void)
import Data.Aeson
import Data.Binary.Builder (fromLazyByteString)
import Data.IORef
import Data.Map qualified as Map
import Data.Set qualified as Set
import Data.Time (LocalTime)
import Data.Word (Word16)
import Network.HTTP.Types (status200, status404)
import Network.Wai (Application, responseLBS, pathInfo, requestMethod)
import Network.Wai.EventSource (ServerEvent(..), eventSourceAppIO)
import Network.Wai.Handler.Warp (run)
import System.FilePath ((</>))
import UnliftIO.STM (atomically, newBroadcastTChanIO, writeTChan, dupTChan, readTChan)

import Echidna.Types.Config (Env(..))
import EVM.Dapp (DappInfo(..))

import Echidna.Output.Corpus (loadTxs)
import Echidna.Output.Source (saveLcovHook)
import Echidna.Types.Campaign (CampaignConf(..))
import Echidna.Types.Config (Env(..), EConfig(..))
import Echidna.Types.Worker
import Echidna.Worker()

newtype SSE = SSE (LocalTime, CampaignEvent)

Expand Down Expand Up @@ -40,36 +51,84 @@ runSSEServer :: MVar () -> Env -> Word16 -> Int -> IO ()
runSSEServer serverStopVar env port nworkers = do
aliveRef <- newIORef nworkers
sseChan <- dupChan env.eventQueue
broadcastChan <- newBroadcastTChanIO

void . forkIO . forever $ do
event@(_, campaignEvent) <- readChan sseChan
let eventName = \case
WorkerEvent _ _ workerEvent ->
case workerEvent of
TestFalsified _ -> "test_falsified"
TestOptimized _ -> "test_optimized"
NewCoverage {} -> "new_coverage"
SymExecLog _ -> "sym_exec_log"
SymExecError _ -> "sym_exec_error"
TxSequenceReplayed {} -> "tx_sequence_replayed"
TxSequenceReplayFailed {} -> "tx_sequence_replay_failed"
WorkerStopped _ -> "worker_stopped"
Failure _err -> "failure"
ReproducerSaved _ -> "saved_reproducer"

let serverEvent = ServerEvent
{ eventName = Just (eventName campaignEvent)
, eventId = Nothing
, eventData = [ fromLazyByteString $ encode (SSE event) ]
}

atomically $ writeTChan broadcastChan serverEvent

case campaignEvent of
WorkerEvent _ _ (WorkerStopped _) -> do
aliveAfter <- atomicModifyIORef' aliveRef (\n -> (n-1, n-1))
when (aliveAfter == 0) $ do
atomically $ writeTChan broadcastChan CloseEvent
putMVar serverStopVar ()
_ -> pure ()

let sseApp _req respond = do
myChan <- atomically $ dupTChan broadcastChan
let src = atomically $ readTChan myChan
eventSourceAppIO src _req respond

let app :: Application
app req respond = case (requestMethod req, pathInfo req) of
-- SSE endpoint
("GET", ["events"]) -> sseApp req respond

-- Dump LCOV coverage
("POST", ["dump_lcov"]) -> do
case env.cfg.campaignConf.corpusDir of
Nothing ->
respond $ responseLBS status404 [("Content-Type", "application/json")]
"{\"error\":\"No corpus directory configured\"}"
Just dir -> do
let contracts = Map.elems env.dapp.solcByName
fn <- saveLcovHook env dir env.sourceCache contracts
respond $ responseLBS status200 [("Content-Type", "application/json")]
(encode $ object ["file" .= fn])

-- Reload corpus from disk
("POST", ["reload_corpus"]) -> do
case env.cfg.campaignConf.corpusDir of
Nothing ->
respond $ responseLBS status404 [("Content-Type", "application/json")]
"{\"error\":\"No corpus directory configured\"}"
Just dir -> do
-- Load transactions from reproducers and coverage directories
ctxs1 <- loadTxs (dir </> "reproducers")
ctxs2 <- loadTxs (dir </> "coverage")
let allTxs = ctxs1 ++ ctxs2
-- Add to corpus (with index based on current corpus size)
loaded <- atomicModifyIORef' env.corpusRef $ \corpus ->
let newEntries = Set.fromList $ zip [Set.size corpus + 1..] (map snd allTxs)
!corpus' = force $ Set.union corpus newEntries
in (corpus', length allTxs)
respond $ responseLBS status200 [("Content-Type", "application/json")]
(encode $ object ["loaded" .= loaded, "message" .= ("Loaded " ++ show loaded ++ " transaction sequences" :: String)])

let sseListener = do
aliveNow <- readIORef aliveRef
if aliveNow == 0 then
pure CloseEvent
else do
event@(_, campaignEvent) <- readChan sseChan
let eventName = \case
WorkerEvent _ _ workerEvent ->
case workerEvent of
TestFalsified _ -> "test_falsified"
TestOptimized _ -> "test_optimized"
NewCoverage {} -> "new_coverage"
SymExecLog _ -> "sym_exec_log"
SymExecError _ -> "sym_exec_error"
TxSequenceReplayed {} -> "tx_sequence_replayed"
TxSequenceReplayFailed {} -> "tx_sequence_replay_failed"
WorkerStopped _ -> "worker_stopped"
Failure _err -> "failure"
ReproducerSaved _ -> "saved_reproducer"
case campaignEvent of
WorkerEvent _ _ (WorkerStopped _) -> do
aliveAfter <- atomicModifyIORef' aliveRef (\n -> (n-1, n-1))
when (aliveAfter == 0) $ putMVar serverStopVar ()
_ -> pure ()
pure $ ServerEvent
{ eventName = Just (eventName campaignEvent)
, eventId = Nothing
, eventData = [ fromLazyByteString $ encode (SSE event) ]
}
-- Unknown endpoint
_ -> respond $ responseLBS status404 [("Content-Type", "application/json")]
"{\"error\":\"Not found\"}"

void . forkIO $ do
run (fromIntegral port) $ eventSourceAppIO sseListener
run (fromIntegral port) app
2 changes: 2 additions & 0 deletions lib/Echidna/Types/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Data.Word (Word64)

import EVM.Dapp (DappInfo)
import EVM.Fetch qualified as Fetch
import EVM.Solidity (SourceCache)
import EVM.Types (Addr, W256)

import Echidna.SourceAnalysis.Slither (SlitherInfo)
Expand Down Expand Up @@ -69,6 +70,7 @@ data EConfigWithUsage = EConfigWithUsage
data Env = Env
{ cfg :: EConfig
, dapp :: DappInfo
, sourceCache :: SourceCache

-- | Shared between all workers. Events are fairly rare so contention is
-- minimal.
Expand Down
2 changes: 2 additions & 0 deletions package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ library:
- vector
- vty
- vty-crossplatform
- wai
- wai-extra
- warp
- http-types
- wreq
- word-wrap
- xml-conduit
Expand Down
Loading