Skip to content

Commit

Permalink
Merge pull request #61 from crytic/echidna-dev-etheno
Browse files Browse the repository at this point in the history
Preliminary support for the Echidna refactor
  • Loading branch information
ESultanik authored Apr 12, 2019
2 parents 2c5642d + 5c3ccb5 commit 0ce12c5
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 50 deletions.
21 changes: 19 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
29 changes: 18 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,33 @@ 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
WORKDIR /home/etheno/echidna
# Etheno currently requires the dev-no-hedgehog branch;
RUN git checkout dev-no-hedgehog
# Etheno currently requires the dev-etheno branch;
RUN git checkout dev-etheno
RUN stack upgrade
RUN stack setup
RUN stack install
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
Expand All @@ -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

Expand Down
21 changes: 15 additions & 6 deletions etheno/echidna.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -49,10 +52,11 @@ 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)



def decode_binary_json(text):
orig = text
text = decode(text).strip()
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
33 changes: 17 additions & 16 deletions etheno/etheno.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
31 changes: 19 additions & 12 deletions etheno/genesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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': {
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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
4 changes: 2 additions & 2 deletions examples/ConstantinopleGasUsage/constantinople.sol
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down

0 comments on commit 0ce12c5

Please sign in to comment.