Skip to content

Commit

Permalink
Merge pull request #438 from ethereum/bitwuza-v2
Browse files Browse the repository at this point in the history
add support for bitwuzla
  • Loading branch information
d-xo authored Jan 16, 2024
2 parents ea7a383 + 9644eae commit 7f41208
Show file tree
Hide file tree
Showing 10 changed files with 69 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Added

- Support for using Bitwuzla as a solver
- Symbolic tests now support statically sized arrays as parameters
- `hevm test` now has a `num-solvers` parameter that controls how many solver instances to spawn
- New solc-specific simplification rules that should make the final Props a lot more readable
Expand Down
7 changes: 4 additions & 3 deletions cli/cli.hs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ data Command w
, showReachableTree :: w ::: Bool <?> "Print only reachable branches explored in tree view"
, smttimeout :: w ::: Maybe Natural <?> "Timeout given to SMT solver in seconds (default: 300)"
, maxIterations :: w ::: Maybe Integer <?> "Number of times we may revisit a particular branching point"
, solver :: w ::: Maybe Text <?> "Used SMT solver: z3 (default) or cvc5"
, solver :: w ::: Maybe Text <?> "Used SMT solver: z3 (default), cvc5, or bitwuzla"
, smtdebug :: w ::: Bool <?> "Print smt queries sent to the solver"
, debug :: w ::: Bool <?> "Debug printing of internal behaviour"
, trace :: w ::: Bool <?> "Dump trace"
Expand All @@ -104,7 +104,7 @@ data Command w
, calldata :: w ::: Maybe ByteString <?> "Tx: calldata"
, smttimeout :: w ::: Maybe Natural <?> "Timeout given to SMT solver in seconds (default: 300)"
, maxIterations :: w ::: Maybe Integer <?> "Number of times we may revisit a particular branching point"
, solver :: w ::: Maybe Text <?> "Used SMT solver: z3 (default) or cvc5"
, solver :: w ::: Maybe Text <?> "Used SMT solver: z3 (default), cvc5, or bitwuzla"
, smtoutput :: w ::: Bool <?> "Print verbose smt output"
, smtdebug :: w ::: Bool <?> "Print smt queries sent to the solver"
, debug :: w ::: Bool <?> "Debug printing of internal behaviour"
Expand Down Expand Up @@ -150,7 +150,7 @@ data Command w
, verbose :: w ::: Maybe Int <?> "Append call trace: {1} failures {2} all"
, coverage :: w ::: Bool <?> "Coverage analysis"
, match :: w ::: Maybe String <?> "Test case filter - only run methods matching regex"
, solver :: w ::: Maybe Text <?> "Used SMT solver: z3 (default) or cvc5"
, solver :: w ::: Maybe Text <?> "Used SMT solver: z3 (default), cvc5, or bitwuzla"
, numSolvers :: w ::: Maybe Natural <?> "Number of solver instances to use (default: number of cpu cores)"
, smtdebug :: w ::: Bool <?> "Print smt queries sent to the solver"
, debug :: w ::: Bool <?> "Debug printing of internal behaviour"
Expand Down Expand Up @@ -265,6 +265,7 @@ getSolver cmd = case cmd.solver of
Just s -> case T.unpack s of
"z3" -> pure Z3
"cvc5" -> pure CVC5
"bitwuzla" -> pure Bitwuzla
input -> do
putStrLn $ "unrecognised solver: " <> input
exitFailure
Expand Down
2 changes: 1 addition & 1 deletion doc/src/equivalence.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Available options:
--smttimeout NATURAL Timeout given to SMT solver in seconds (default: 300)
--max-iterations INTEGER Number of times we may revisit a particular branching
point
--solver TEXT Used SMT solver: z3 (default) or cvc5
--solver TEXT Used SMT solver: z3 (default), cvc5, or bitwuzla
--smtoutput Print verbose smt output
--smtdebug Print smt queries sent to the solver
--ask-smt-iterations INTEGER
Expand Down
2 changes: 1 addition & 1 deletion doc/src/symbolic.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Available options:
--smttimeout NATURAL Timeout given to SMT solver in seconds (default: 300)
--max-iterations INTEGER Number of times we may revisit a particular branching
point
--solver TEXT Used SMT solver: z3 (default) or cvc5
--solver TEXT Used SMT solver: z3 (default), cvc5, or bitwuzla
--smtdebug Print smt queries sent to the solver
--assertions [WORD256] Comma separated list of solc panic codes to check for
(default: user defined assertion violations only)
Expand Down
5 changes: 3 additions & 2 deletions doc/src/test.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `hevm dapp-test`
# `hevm test`

```
Usage: hevm test [--root STRING] [--project-type PROJECTTYPE] [--rpc TEXT]
Expand All @@ -18,7 +18,8 @@ Available options:
--verbose INT Append call trace: {1} failures {2} all
--coverage Coverage analysis
--match STRING Test case filter - only run methods matching regex
--solver TEXT Used SMT solver: z3 (default) or cvc5
--solver TEXT Used SMT solver: z3 (default), cvc5, or bitwuzla
--num-solvers NATURAL Number of solver instances to use (default: number of cpu cores)
--smtdebug Print smt queries sent to the solver
--ffi Allow the usage of the hevm.ffi() cheatcode (WARNING:
Expand Down
17 changes: 17 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
foundry.url = "github:shazow/foundry.nix/monthly";
bitwuzla-pkgs.url = "github:d-xo/nixpkgs/6e7c9e4267f3c2df116bf76d8e31c2602e2d543d";
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
Expand All @@ -27,16 +28,18 @@
};
};

outputs = { self, nixpkgs, flake-utils, solidity, forge-std, ethereum-tests, foundry, cabal-head, ... }:
outputs = { self, nixpkgs, flake-utils, solidity, forge-std, ethereum-tests, foundry, cabal-head, bitwuzla-pkgs, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = (import nixpkgs { inherit system; config = { allowBroken = true; }; });
bitwuzla = (import bitwuzla-pkgs { inherit system; }).bitwuzla;
testDeps = with pkgs; [
go-ethereum
solc
z3
cvc5
git
bitwuzla
] ++ lib.optional (!(pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64)) [
foundry.defaultPackage.${system}
];
Expand Down Expand Up @@ -126,7 +129,7 @@
buildInputs = [ makeWrapper ];
postBuild = ''
wrapProgram $out/bin/hevm \
--prefix PATH : "${lib.makeBinPath ([ bash coreutils git solc z3 cvc5 ])}"
--prefix PATH : "${lib.makeBinPath ([ bash coreutils git solc z3 cvc5 bitwuzla ])}"
'';
};

Expand Down
6 changes: 3 additions & 3 deletions src/EVM/SMT.hs
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,11 @@ declareIntermediates bufs stores =
where
compareFst (l, _) (r, _) = compare l r
encodeBuf n expr =
SMT2 ["(define-const buf" <> (fromString . show $ n) <> " Buf " <> exprToSMT expr <> ")\n"] mempty mempty mempty <> encodeBufLen n expr
SMT2 ["(define-fun buf" <> (fromString . show $ n) <> "() Buf " <> exprToSMT expr <> ")\n"] mempty mempty mempty <> encodeBufLen n expr
encodeBufLen n expr =
SMT2 ["(define-const buf" <> (fromString . show $ n) <>"_length" <> " (_ BitVec 256) " <> exprToSMT (bufLengthEnv bufs True expr) <> ")"] mempty mempty mempty
SMT2 ["(define-fun buf" <> (fromString . show $ n) <>"_length () (_ BitVec 256) " <> exprToSMT (bufLengthEnv bufs True expr) <> ")"] mempty mempty mempty
encodeStore n expr =
SMT2 ["(define-const store" <> (fromString . show $ n) <> " Storage " <> exprToSMT expr <> ")"] mempty mempty mempty
SMT2 ["(define-fun store" <> (fromString . show $ n) <> " () Storage " <> exprToSMT expr <> ")"] mempty mempty mempty

data AbstState = AbstState
{ words :: Map (Expr EWord) Int
Expand Down
40 changes: 27 additions & 13 deletions src/EVM/Solvers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Data.Text.Lazy (Text)
import Data.Text.Lazy qualified as T
import Data.Text.Lazy.IO qualified as T
import Data.Text.Lazy.Builder
import System.Process (createProcess, cleanupProcess, proc, ProcessHandle, std_in, std_out, std_err, StdStream(..))
import System.Process (createProcess, cleanupProcess, proc, ProcessHandle, std_in, std_out, std_err, StdStream(..), createPipe)
import Witch (into)
import EVM.Effects
import EVM.Fuzz (tryCexFuzz)
Expand All @@ -49,7 +49,6 @@ data SolverInstance = SolverInstance
{ solvertype :: Solver
, stdin :: Handle
, stdout :: Handle
, stderr :: Handle
, process :: ProcessHandle
}

Expand Down Expand Up @@ -221,15 +220,15 @@ getModel inst cexvars = do
shrinkBuf buf hint = do
let encBound = "(_ bv" <> (T.pack $ show (into hint :: Integer)) <> " 256)"
sat <- liftIO $ do
checkCommand inst "(push)"
checkCommand inst "(push 1)"
checkCommand inst $ "(assert (bvule " <> buf <> "_length " <> encBound <> "))"
sendLine inst "(check-sat)"
case sat of
"sat" -> do
model <- liftIO getRaw
put model
"unsat" -> do
liftIO $ checkCommand inst "(pop)"
liftIO $ checkCommand inst "(pop 1)"
shrinkBuf buf (if hint == 0 then hint + 1 else hint * 2)
e -> internalError $ "Unexpected solver output: " <> (T.unpack e)

Expand Down Expand Up @@ -257,7 +256,12 @@ mkTimeout t = T.pack $ show $ (1000 *)$ case t of
-- | Arguments used when spawing a solver instance
solverArgs :: Solver -> Maybe Natural -> [Text]
solverArgs solver timeout = case solver of
Bitwuzla -> internalError "TODO: Bitwuzla args"
Bitwuzla ->
[ "--lang=smt2"
, "--produce-models"
, "--time-limit-per=" <> mkTimeout timeout
, "--bv-solver=preprop"
]
Z3 ->
[ "-in" ]
CVC5 ->
Expand All @@ -272,21 +276,31 @@ solverArgs solver timeout = case solver of
-- | Spawns a solver instance, and sets the various global config options that we use for our queries
spawnSolver :: Solver -> Maybe (Natural) -> IO SolverInstance
spawnSolver solver timeout = do
let cmd = (proc (show solver) (fmap T.unpack $ solverArgs solver timeout)) { std_in = CreatePipe, std_out = CreatePipe, std_err = CreatePipe }
(Just stdin, Just stdout, Just stderr, process) <- createProcess cmd
(readout, writeout) <- createPipe
let cmd
= (proc (show solver) (fmap T.unpack $ solverArgs solver timeout))
{ std_in = CreatePipe
, std_out = UseHandle writeout
, std_err = UseHandle writeout
}
(Just stdin, Nothing, Nothing, process) <- createProcess cmd
hSetBuffering stdin (BlockBuffering (Just 1000000))
let solverInstance = SolverInstance solver stdin stdout stderr process
let solverInstance = SolverInstance solver stdin readout process

case solver of
CVC5 -> pure solverInstance
_ -> do
Bitwuzla -> do
_ <- sendLine solverInstance "(set-option :print-success true)"
pure solverInstance
Z3 -> do
_ <- sendLine' solverInstance $ "(set-option :timeout " <> mkTimeout timeout <> ")"
_ <- sendLine solverInstance "(set-option :print-success true)"
pure solverInstance
Custom _ -> pure solverInstance

-- | Cleanly shutdown a running solver instnace
stopSolver :: SolverInstance -> IO ()
stopSolver (SolverInstance _ stdin stdout stderr process) = cleanupProcess (Just stdin, Just stdout, Just stderr, process)
stopSolver (SolverInstance _ stdin stdout process) = cleanupProcess (Just stdin, Just stdout, Nothing, process)

-- | Sends a list of commands to the solver. Returns the first error, if there was one.
sendScript :: SolverInstance -> SMT2 -> IO (Either Text ())
Expand Down Expand Up @@ -320,20 +334,20 @@ sendCommand inst cmd = do

-- | Sends a string to the solver and appends a newline, returns the first available line from the output buffer
sendLine :: SolverInstance -> Text -> IO Text
sendLine (SolverInstance _ stdin stdout _ _) cmd = do
sendLine (SolverInstance _ stdin stdout _) cmd = do
T.hPutStr stdin (T.append cmd "\n")
hFlush stdin
T.hGetLine stdout

-- | Sends a string to the solver and appends a newline, doesn't return stdout
sendLine' :: SolverInstance -> Text -> IO ()
sendLine' (SolverInstance _ stdin _ _ _) cmd = do
sendLine' (SolverInstance _ stdin _ _) cmd = do
T.hPutStr stdin (T.append cmd "\n")
hFlush stdin

-- | Returns a string representation of the model for the requested variable
getValue :: SolverInstance -> Text -> IO Text
getValue (SolverInstance _ stdin stdout _ _) var = do
getValue (SolverInstance _ stdin stdout _) var = do
T.hPutStr stdin (T.append (T.append "(get-value (" var) "))\n")
hFlush stdin
fmap (T.unlines . reverse) (readSExpr stdout)
Expand Down
14 changes: 7 additions & 7 deletions test/test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ tests = testGroup "hevm"
}
}
|]
(_, [Cex (_, ctr)]) <- withSolvers Z3 1 Nothing $ \s -> checkAssert s [0x12] c (Just (Sig "fun(uint256,uint256)" [AbiUIntType 256, AbiUIntType 256])) [] defaultVeriOpts
(_, [Cex (_, ctr)]) <- withSolvers Bitwuzla 1 Nothing $ \s -> checkAssert s [0x12] c (Just (Sig "fun(uint256,uint256)" [AbiUIntType 256, AbiUIntType 256])) [] defaultVeriOpts
assertEqualM "Division by 0 needs b=0" (getVar ctr "arg2") 0
putStrLnM "expected counterexample found"
,
Expand All @@ -828,7 +828,7 @@ tests = testGroup "hevm"
}
}
|]
(_, [Cex _]) <- withSolvers Z3 1 Nothing $ \s -> checkAssert s [0x1] c Nothing [] defaultVeriOpts
(_, [Cex _]) <- withSolvers Bitwuzla 1 Nothing $ \s -> checkAssert s [0x1] c Nothing [] defaultVeriOpts
putStrLnM "expected counterexample found"
,
test "enum-conversion-fail" $ do
Expand All @@ -841,7 +841,7 @@ tests = testGroup "hevm"
}
}
|]
(_, [Cex (_, ctr)]) <- withSolvers Z3 1 Nothing $ \s -> checkAssert s [0x21] c (Just (Sig "fun(uint256)" [AbiUIntType 256])) [] defaultVeriOpts
(_, [Cex (_, ctr)]) <- withSolvers Bitwuzla 1 Nothing $ \s -> checkAssert s [0x21] c (Just (Sig "fun(uint256)" [AbiUIntType 256])) [] defaultVeriOpts
assertBoolM "Enum is only defined for 0 and 1" $ (getVar ctr "arg1") > 1
putStrLnM "expected counterexample found"
,
Expand Down Expand Up @@ -1010,7 +1010,7 @@ tests = testGroup "hevm"
}
}
|]
withSolvers Z3 1 Nothing $ \s -> do
withSolvers Bitwuzla 1 Nothing $ \s -> do
let calldata = (WriteWord (Lit 0x0) (Var "u") (ConcreteBuf ""), [])
initVM <- liftIO $ stToIO $ abstractVM calldata initCode Nothing True
expr <- Expr.simplify <$> interpret (Fetch.oracle s Nothing) Nothing 1 StackBased initVM runExpr
Expand Down Expand Up @@ -2655,7 +2655,7 @@ tests = testGroup "hevm"
}
|]

(_, [Qed _]) <- withSolvers CVC5 1 (Just 99999999) $ \s -> checkAssert s defaultPanicCodes c (Just (Sig "distributivity(uint256,uint256,uint256)" [AbiUIntType 256, AbiUIntType 256, AbiUIntType 256])) [] defaultVeriOpts
(_, [Qed _]) <- withSolvers Bitwuzla 1 (Just 99999999) $ \s -> checkAssert s defaultPanicCodes c (Just (Sig "distributivity(uint256,uint256,uint256)" [AbiUIntType 256, AbiUIntType 256, AbiUIntType 256])) [] defaultVeriOpts
putStrLnM "Proven"
,
test "storage-cex-1" $ do
Expand Down Expand Up @@ -2810,7 +2810,7 @@ tests = testGroup "hevm"
}
|]
let sig = (Sig "func(uint256,uint256)" [AbiUIntType 256, AbiUIntType 256])
(_, [Cex (_, ctr1), Cex (_, ctr2)]) <- withSolvers CVC5 1 Nothing $ \s -> checkAssert s defaultPanicCodes c (Just sig) [] defaultVeriOpts
(_, [Cex (_, ctr1), Cex (_, ctr2)]) <- withSolvers Bitwuzla 1 Nothing $ \s -> checkAssert s defaultPanicCodes c (Just sig) [] defaultVeriOpts
putStrLnM $ "expected counterexamples found. ctr1: " <> (show ctr1) <> " ctr2: " <> (show ctr2)
, testFuzz "fuzz-simple-fixed-value-store1" $ do
Just c <- solcRuntime "MyContract"
Expand Down Expand Up @@ -3096,7 +3096,7 @@ tests = testGroup "hevm"
}
}
|]
withSolvers Z3 3 Nothing $ \s -> do
withSolvers Bitwuzla 3 Nothing $ \s -> do
a <- equivalenceCheck s aPrgm bPrgm defaultVeriOpts (mkCalldata Nothing [])
assertEqualM "Must be different" (any isCex a) True
, test "eq-all-yul-optimization-tests" $ do
Expand Down

0 comments on commit 7f41208

Please sign in to comment.