From ba1078556c7fa07a437aaee0eafdb31ba6e9c44c Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Wed, 10 Apr 2019 14:56:49 -0400 Subject: [PATCH 1/4] Support for the new Echidna `dev-etheno` branch --- Dockerfile | 25 +++++++++----- etheno/echidna.py | 17 +++++++--- etheno/etheno.py | 33 ++++++++++--------- etheno/genesis.py | 31 ++++++++++------- .../ConstantinopleGasUsage/constantinople.sol | 4 +-- 5 files changed, 67 insertions(+), 43 deletions(-) diff --git a/Dockerfile b/Dockerfile index a311bab..b69dd3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,7 @@ USER etheno RUN git clone https://github.com/trailofbits/echidna.git WORKDIR /home/etheno/echidna # Etheno currently requires the dev-no-hedgehog branch; -RUN git checkout dev-no-hedgehog +RUN git checkout dev-etheno RUN stack upgrade RUN stack setup RUN stack install @@ -43,6 +43,19 @@ WORKDIR /home/etheno # END Install Echidna +USER root + +# Install Parity +RUN apt-get install -y cmake libudev-dev +RUN curl https://get.parity.io -L | bash + +# Allow passwordless sudo for etheno +RUN echo 'etheno ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers + +RUN chown -R etheno:etheno /home/etheno/ + +USER etheno + RUN mkdir -p /home/etheno/etheno/etheno COPY LICENSE /home/etheno/etheno @@ -57,14 +70,8 @@ RUN cd etheno && pip3 install --user '.[manticore]' USER root -# Install Parity -RUN apt-get install -y cmake libudev-dev -RUN curl https://get.parity.io -L | bash - -# Allow passwordless sudo for etheno -RUN echo 'etheno ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers - -RUN chown -R etheno:etheno /home/etheno/ +RUN chown -R etheno:etheno /home/etheno/etheno +RUN chown -R etheno:etheno /home/etheno/examples USER etheno diff --git a/etheno/echidna.py b/etheno/echidna.py index bd47eed..d352fa3 100644 --- a/etheno/echidna.py +++ b/etheno/echidna.py @@ -3,7 +3,6 @@ import tempfile from .ascii_escapes import decode -from .client import JSONRPCError from .etheno import EthenoPlugin from .utils import ConstantTemporaryFile, format_hex_address @@ -28,17 +27,21 @@ } ''' -ECHIDNA_CONFIG = b'''outputRawTxs: True\ngasLimit: 0xfffff\n''' +ECHIDNA_CONFIG = b'''outputRawTxs: true\nquiet: true\ndashboard: false\ngasLimit: 0xfffff\n''' + def echidna_exists(): return subprocess.call(['/usr/bin/env', 'echidna-test', '--help'], stdout=subprocess.DEVNULL) == 0 + def stack_exists(): return subprocess.call(['/usr/bin/env', 'stack', '--help'], stdout=subprocess.DEVNULL) == 0 + def git_exists(): return subprocess.call(['/usr/bin/env', 'git', '--version'], stdout=subprocess.DEVNULL) == 0 + def install_echidna(allow_reinstall = False): if not allow_reinstall and echidna_exists(): return @@ -52,7 +55,8 @@ def install_echidna(allow_reinstall = False): # TODO: Once the `dev-no-hedgehog` branch is merged into `master`, we can remove this: subprocess.call(['/usr/bin/env', 'git', 'checkout', 'dev-no-hedgehog'], cwd=path) subprocess.check_call(['/usr/bin/env', 'stack', 'install'], cwd=path) - + + def decode_binary_json(text): orig = text text = decode(text).strip() @@ -73,6 +77,7 @@ def decode_binary_json(text): raise ValueError("Malformed JSON list! Expected '%s' but instead got '%s' at offset %d" % ('"', chr(text[-1]), offset + len(text) - 1)) return text[:-1] + class EchidnaPlugin(EthenoPlugin): def __init__(self, transaction_limit=None, contract_source=None): self._transaction = 0 @@ -93,6 +98,9 @@ def run(self): self.logger.info("Etheno does not know about any accounts, so Echidna has nothing to do!") self._shutdown() return + elif self.contract_source is None: + self.logger.error("Error compiling source contract") + self._shutdown() # First, deploy the testing contract: self.logger.info('Deploying Echidna test contract...') self.contract_address = format_hex_address(self.etheno.deploy_contract(self.etheno.accounts[0], self.contract_bytecode), True) @@ -172,5 +180,6 @@ def emit_transaction(self, txn): self.logger.info("Emitting Transaction %d" % self._transaction) self.etheno.post(transaction) + if __name__ == '__main__': - install_echidna(allow_reinstall = True) + install_echidna(allow_reinstall=True) diff --git a/etheno/etheno.py b/etheno/etheno.py index 4fc0f61..00d88d7 100644 --- a/etheno/etheno.py +++ b/etheno/etheno.py @@ -57,54 +57,55 @@ def etheno(self, instance): @property def log_directory(self): - '''Returns a log directory that this client can use to save additional files, or None if one is not available''' + """Returns a log directory that this client can use to save additional files, or None if one is not available""" if self.logger is None: return None else: return self.logger.directory def added(self): - ''' + """ A callback when this plugin is added to an Etheno instance - ''' + """ pass def before_post(self, post_data): - ''' + """ A callback when Etheno receives a JSON RPC POST, but before it is processed. :param post_data: The raw JSON RPC data :return: the post_data to be used by Etheno (can be modified) - ''' + """ pass def after_post(self, post_data, client_results): - ''' + """ A callback when Etheno receives a JSON RPC POST after it is processed by all clients. :param post_data: The raw JSON RPC data :param client_results: A lost of the results returned by each client - ''' + """ pass def run(self): - ''' + """ A callback when Etheno is running and all other clients and plugins are initialized - ''' + """ pass def finalize(self): - ''' + """ Called when an analysis pass should be finalized (e.g., after a Truffle migration completes). Subclasses implementing this function should support it to be called multiple times in a row. - ''' + """ pass def shutdown(self): - ''' + """ Called before Etheno shuts down. The default implementation calls `finalize()`. - ''' + """ self.finalize() + class Etheno(object): def __init__(self, master_client=None): self.accounts = [] @@ -151,11 +152,11 @@ def master_client(self, client): self._create_accounts(client) def estimate_gas(self, transaction): - ''' + """ Estimates the gas cost of a transaction. Iterates through all clients until it finds a client that is capable of estimating the gas cost without error. If all clients return an error, this function will return None. - ''' + """ clients = [self.master_client] + self.clients for client in clients: try: @@ -261,7 +262,7 @@ def add_client(self, client): self.clients.append(client) self._create_accounts(client) - def deploy_contract(self, from_address, bytecode, gas = 0x99999, gas_price = None, value = 0): + def deploy_contract(self, from_address, bytecode, gas=0x99999, gas_price=None, value=0): if gas_price is None: gas_price = self.master_client.get_gas_price() if isinstance(bytecode, bytes): diff --git a/etheno/genesis.py b/etheno/genesis.py index 9386bce..68f0d3d 100644 --- a/etheno/genesis.py +++ b/etheno/genesis.py @@ -2,18 +2,22 @@ from .utils import format_hex_address + class Account(object): def __init__(self, address, balance = None, private_key = None): self._address = address self.balance = balance self._private_key = private_key + @property def address(self): return self._address + @property def private_key(self): return self._private_key + def make_genesis(network_id=0x657468656E6F, difficulty=20, gas_limit=200000000000, accounts=None, byzantium_block=0, dao_fork_block=0, homestead_block=0, eip150_block=0, eip155_block=0, eip158_block=0, constantinople_block=None): if accounts: alloc = {format_hex_address(acct.address): {'balance': "%d" % acct.balance, 'privateKey': format_hex_address(acct.private_key)} for acct in accounts} @@ -40,8 +44,9 @@ def make_genesis(network_id=0x657468656E6F, difficulty=20, gas_limit=20000000000 return ret + def geth_to_parity(genesis): - '''Converts a Geth style genesis to Parity style''' + """Converts a Geth style genesis to Parity style""" ret = { 'name': 'etheno', 'engine': { @@ -59,16 +64,17 @@ def geth_to_parity(genesis): # } }, 'genesis': { - "seal": { "generic": "0x0" - #'ethereum': { - # 'nonce': '0x0000000000000042', - # 'mixHash': '0x0000000000000000000000000000000000000000000000000000000000000000' - #} - }, - 'difficulty': "0x%s" % genesis['difficulty'], - 'gasLimit': "0x%s" % genesis['gasLimit'], + "seal": { + "generic": "0x0" + # 'ethereum': { + # 'nonce': '0x0000000000000042', + # 'mixHash': '0x0000000000000000000000000000000000000000000000000000000000000000' + # } + }, + 'difficulty': "0x%s" % genesis['difficulty'], + 'gasLimit': "0x%s" % genesis['gasLimit'], 'author': list(genesis['alloc'])[-1] - }, + }, 'params': { 'networkID' : "0x%x" % genesis['config']['chainId'], 'maximumExtraDataSize': '0x20', @@ -80,7 +86,7 @@ def geth_to_parity(genesis): 'eip161dTransition': '0x0', 'eip155Transition': "0x%x" % genesis['config']['eip155Block'], 'eip98Transition': '0x7fffffffffffff', - 'eip86Transition': '0x7fffffffffffff', + # 'eip86Transition': '0x7fffffffffffff', 'maxCodeSize': 24576, 'maxCodeSizeTransition': '0x0', 'eip140Transition': '0x0', @@ -100,9 +106,10 @@ def geth_to_parity(genesis): return ret + def make_accounts(num_accounts, default_balance = None): ret = [] for i in range(num_accounts): acct = w3.eth.account.create() - ret.append(Account(address = int(acct.address, 16), private_key = int(acct.privateKey.hex(), 16), balance = default_balance)) + ret.append(Account(address=int(acct.address, 16), private_key=int(acct.privateKey.hex(), 16), balance=default_balance)) return ret diff --git a/examples/ConstantinopleGasUsage/constantinople.sol b/examples/ConstantinopleGasUsage/constantinople.sol index 55b41f6..17a7e57 100644 --- a/examples/ConstantinopleGasUsage/constantinople.sol +++ b/examples/ConstantinopleGasUsage/constantinople.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.24; +pragma solidity ^0.5.4; contract C { int public stored = 1337; function setStored(int value) public { @@ -7,7 +7,7 @@ contract C { function increment() public { int newValue = stored + 1; stored = 0; - address(this).call(bytes4(keccak256("setStored(int256)")), newValue); + address(this).call(abi.encodeWithSignature("setStored(int256)", newValue)); } function echidna_() public returns (bool) { return true; From 96392c7ab1a470409e43d2d21ab34e35d42525d1 Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Thu, 11 Apr 2019 10:24:55 -0400 Subject: [PATCH 2/4] `dev-no-hedgehog` --> `dev-etheno` --- Dockerfile | 2 +- etheno/echidna.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index b69dd3c..01372c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ RUN curl -sSL https://get.haskellstack.org/ | sh USER etheno RUN git clone https://github.com/trailofbits/echidna.git WORKDIR /home/etheno/echidna -# Etheno currently requires the dev-no-hedgehog branch; +# Etheno currently requires the dev-etheno branch; RUN git checkout dev-etheno RUN stack upgrade RUN stack setup diff --git a/etheno/echidna.py b/etheno/echidna.py index d352fa3..98f3d7c 100644 --- a/etheno/echidna.py +++ b/etheno/echidna.py @@ -52,8 +52,8 @@ def install_echidna(allow_reinstall = False): with tempfile.TemporaryDirectory() as path: subprocess.check_call(['/usr/bin/env', 'git', 'clone', 'https://github.com/trailofbits/echidna.git', path]) - # TODO: Once the `dev-no-hedgehog` branch is merged into `master`, we can remove this: - subprocess.call(['/usr/bin/env', 'git', 'checkout', 'dev-no-hedgehog'], cwd=path) + # TODO: Once the `dev-etheno` branch is merged into `master`, we can remove this: + subprocess.call(['/usr/bin/env', 'git', 'checkout', 'dev-etheno'], cwd=path) subprocess.check_call(['/usr/bin/env', 'stack', 'install'], cwd=path) From 5f81dd46eef8e465fa09ba7952c342b702a7e813 Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Thu, 11 Apr 2019 10:34:01 -0400 Subject: [PATCH 3/4] Update to version `0.2.2` --- CHANGELOG.md | 21 +++++++++++++++++++-- setup.py | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e1717..bfd4dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,26 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). -## [Unreleased](https://github.com/trailofbits/etheno/compare/v0.2.0...HEAD) +## [Unreleased](https://github.com/trailofbits/etheno/compare/v0.2.2...HEAD) -### 0.2.1 — 2019-02-07 +## 0.2.2 — 2019-04-11 + +### Added + +- Updated to support a [newer version of Echidna](https://github.com/crytic/echidna/tree/dev-etheno) + - We are almost at feature parity with Echidna master, which we expect to happen at the next release +- Two new commandline options to export raw transactions as a JSON file +- New `--truffle-cmd` argument to specify the build command + +### Changed + +- The [`BrokenMetaCoin` example](examples/BrokenMetaCoin) was updated to a newer version of Solidity + +### Fixed + +- Fixes a bug in honoring the `--ganache-args` option + +## 0.2.1 — 2019-02-07 Bugfix release. diff --git a/setup.py b/setup.py index 67d745d..8c31c7b 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ description='Etheno is a JSON RPC multiplexer, Manticore wrapper, differential fuzzer, and test framework integration tool.', url='https://github.com/trailofbits/etheno', author='Trail of Bits', - version='0.2.1', + version='0.2.2', packages=find_packages(), python_requires='>=3.6', install_requires=[ From 5c3ccb5ae24502900c63b77935a1639aadd54e61 Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Thu, 11 Apr 2019 15:06:05 -0400 Subject: [PATCH 4/4] Added missing dependencies for Echidna --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 01372c6..1587622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ENV LANG C.UTF-8 # BEGIN Install Echidna USER root -RUN apt-get install -y libgmp-dev libbz2-dev libreadline-dev curl libsecp256k1-dev +RUN apt-get install -y libgmp-dev libbz2-dev libreadline-dev curl libsecp256k1-dev software-properties-common locales-all locales zlib1g-dev RUN curl -sSL https://get.haskellstack.org/ | sh USER etheno RUN git clone https://github.com/trailofbits/echidna.git