diff --git a/.gitignore b/.gitignore index 5ce7483..26a1e17 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,20 @@ -env* +# Bytecode +**/__pycache__/ + +# Development +env*/ +.vscode/ +.DS_Store + +# Distibution +dist/ +*.egg-info/ + +# Testing +.cache/ +.pytest_cache/ +contract_data/ + +# Artifacts +*.pyc +build/ diff --git a/Makefile b/Makefile index e256c7a..21739a0 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ clean-pyc: .PHONY: lint lint: - flake8 contracts utilities + flake8 plasma plasma_core tests .PHONY: test test: diff --git a/contracts/ECRecovery.sol b/plasma/contracts/ECRecovery.sol similarity index 100% rename from contracts/ECRecovery.sol rename to plasma/contracts/ECRecovery.sol diff --git a/contracts/Merkle.sol b/plasma/contracts/Merkle.sol similarity index 100% rename from contracts/Merkle.sol rename to plasma/contracts/Merkle.sol diff --git a/contracts/PlasmaCore.sol b/plasma/contracts/PlasmaCore.sol similarity index 100% rename from contracts/PlasmaCore.sol rename to plasma/contracts/PlasmaCore.sol diff --git a/contracts/PriorityQueue.sol b/plasma/contracts/PriorityQueue.sol similarity index 100% rename from contracts/PriorityQueue.sol rename to plasma/contracts/PriorityQueue.sol diff --git a/contracts/RLP.sol b/plasma/contracts/RLP.sol similarity index 100% rename from contracts/RLP.sol rename to plasma/contracts/RLP.sol diff --git a/contracts/RootChain.sol b/plasma/contracts/RootChain.sol similarity index 97% rename from contracts/RootChain.sol rename to plasma/contracts/RootChain.sol index 0c8dd83..39a1084 100644 --- a/contracts/RootChain.sol +++ b/plasma/contracts/RootChain.sol @@ -96,7 +96,7 @@ contract RootChain { timestamp: block.timestamp }); - emit DepositCreated(msg.sender, msg.value, currentBlockNumber); + emit DepositCreated(msg.sender, msg.value, currentPlasmaBlockNumber); currentPlasmaBlockNumber = currentPlasmaBlockNumber.add(1); } diff --git a/contracts/SafeMath.sol b/plasma/contracts/SafeMath.sol similarity index 100% rename from contracts/SafeMath.sol rename to plasma/contracts/SafeMath.sol diff --git a/utilities/__init__.py b/plasma_core/__init__.py similarity index 100% rename from utilities/__init__.py rename to plasma_core/__init__.py diff --git a/plasma_core/account.py b/plasma_core/account.py new file mode 100644 index 0000000..4a69cee --- /dev/null +++ b/plasma_core/account.py @@ -0,0 +1,5 @@ +class EthereumAccount(object): + + def __init__(self, address, key): + self.address = address + self.key = key diff --git a/plasma_core/constants.py b/plasma_core/constants.py new file mode 100644 index 0000000..2c10c55 --- /dev/null +++ b/plasma_core/constants.py @@ -0,0 +1,3 @@ +NULL_BYTE = b'\x00' +NULL_ADDRESS = NULL_BYTE * 20 +NULL_HASH = NULL_BYTE * 32 diff --git a/plasma_core/utils/__init__.py b/plasma_core/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plasma_core/utils/address.py b/plasma_core/utils/address.py new file mode 100644 index 0000000..8373c50 --- /dev/null +++ b/plasma_core/utils/address.py @@ -0,0 +1,6 @@ +def address_to_hex(address): + return '0x' + address.hex() + + +def address_to_bytes(address): + return bytes.fromhex(address[2:]) diff --git a/plasma_core/utils/deployer.py b/plasma_core/utils/deployer.py new file mode 100644 index 0000000..ad12c64 --- /dev/null +++ b/plasma_core/utils/deployer.py @@ -0,0 +1,149 @@ +import os +import json +from solc import compile_standard +from web3 import Web3, HTTPProvider +from web3.contract import ConciseContract + + +OUTPUT_DIR = 'contract_data' + + +class Deployer(object): + + def __init__(self, contracts_dir, w3=Web3(HTTPProvider('http://localhost:8545'))): + self.contracts_dir = contracts_dir + self.w3 = w3 + + def get_solc_input(self): + """Walks the contract directory and returns a Solidity input dict + + Learn more about Solidity input JSON here: https://goo.gl/7zKBvj + + Returns: + dict: A Solidity input JSON object as a dict + """ + + solc_input = { + 'language': 'Solidity', + 'sources': { + file_name: { + 'urls': [os.path.realpath(os.path.join(r, file_name))] + } for r, d, f in os.walk(self.contracts_dir) for file_name in f + }, + 'settings': { + 'outputSelection': { + "*": { + "": [ + "legacyAST", + "ast" + ], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + } + } + } + + return solc_input + + def compile_all(self): + """Compiles all of the contracts in the /contracts directory + + Creates {contract name}.json files in /build that contain + the build output for each contract. + """ + + # Solidity input JSON + solc_input = self.get_solc_input() + + # Compile the contracts + compilation_result = compile_standard(solc_input, allow_paths=self.contracts_dir) + + # Create the output folder if it doesn't already exist + os.makedirs(OUTPUT_DIR, exist_ok=True) + + # Write the contract ABI to output files + compiled_contracts = compilation_result['contracts'] + for contract_file in compiled_contracts: + for contract in compiled_contracts[contract_file]: + contract_name = contract.split('.')[0] + contract_data = compiled_contracts[contract_file][contract_name] + + contract_data_path = OUTPUT_DIR + '/{0}.json'.format(contract_name) + with open(contract_data_path, "w+") as contract_data_file: + json.dump(contract_data, contract_data_file) + + @staticmethod + def get_contract_data(contract_name): + """Returns the contract data for a given contract + + Args: + contract_name (str): Name of the contract to return. + + Returns: + str, str: ABI and bytecode of the contract + """ + + contract_data_path = OUTPUT_DIR + '/{0}.json'.format(contract_name) + with open(contract_data_path, 'r') as contract_data_file: + contract_data = json.load(contract_data_file) + + abi = contract_data['abi'] + bytecode = contract_data['evm']['bytecode']['object'] + + return abi, bytecode + + def deploy_contract(self, contract_name, gas=5000000, args=(), concise=True): + """Deploys a contract to the given Ethereum network using Web3 + + Args: + contract_name (str): Name of the contract to deploy. Must already be compiled. + provider (HTTPProvider): The Web3 provider to deploy with. + gas (int): Amount of gas to use when creating the contract. + args (obj): Any additional arguments to include with the contract creation. + concise (bool): Whether to return a Contract or ConciseContract instance. + + Returns: + Contract: A Web3 contract instance. + """ + + abi, bytecode = self.get_contract_data(contract_name) + + contract = self.w3.eth.contract(abi=abi, bytecode=bytecode) + + # Get transaction hash from deployed contract + tx_hash = contract.deploy(transaction={ + 'from': self.w3.eth.accounts[0], + 'gas': gas + }, args=args) + + # Get tx receipt to get contract address + tx_receipt = self.w3.eth.getTransactionReceipt(tx_hash) + contract_address = tx_receipt['contractAddress'] + + contract_instance = self.w3.eth.contract(address=contract_address, abi=abi) + + return ConciseContract(contract_instance) if concise else contract_instance + + def get_contract_at_address(self, contract_name, address, concise=True): + """Returns a Web3 instance of the given contract at the given address + + Args: + contract_name (str): Name of the contract. Must already be compiled. + address (str): Address of the contract. + concise (bool): Whether to return a Contract or ConciseContract instance. + + Returns: + Contract: A Web3 contract instance. + """ + + abi, _ = self.get_contract_data(contract_name) + + contract_instance = self.w3.eth.contract(abi=abi, address=address) + + return ConciseContract(contract_instance) if concise else contract_instance diff --git a/setup.py b/setup.py index 0cc5546..b5833d9 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ install_requires=[ 'ethereum==2.3.0', 'rlp==0.6.0', - 'py-solc==3.1.0' + 'py-solc==3.1.0', + 'web3==4.4.1' ] ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..27954d7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,55 @@ +import os +import pytest +from ethereum import utils +from ethereum.abi import ContractTranslator +from ethereum.tools import tester +from ethereum.config import config_metropolis +from plasma_core.account import EthereumAccount +from plasma_core.utils.deployer import Deployer +from plasma_core.utils.address import address_to_hex + + +GAS_LIMIT = 8000000 +START_GAS = GAS_LIMIT - 1000000 +config_metropolis['BLOCK_GAS_LIMIT'] = GAS_LIMIT + + +OWN_DIR = os.path.dirname(os.path.realpath(__file__)) +CONTRACTS_DIR = os.path.abspath(os.path.realpath(os.path.join(OWN_DIR, '../plasma/contracts'))) +deployer = Deployer(CONTRACTS_DIR) +deployer.compile_all() + + +@pytest.fixture +def ethutils(): + return utils + + +@pytest.fixture +def ethtester(): + tester.chain = tester.Chain() + tester.accounts = [] + for i in range(10): + address = getattr(tester, 'a{0}'.format(i)) + key = getattr(tester, 'k{0}'.format(i)) + tester.accounts.append(EthereumAccount(address_to_hex(address), key)) + return tester + + +@pytest.fixture +def get_contract(ethtester, ethutils): + def create_contract(path, args=(), sender=ethtester.k0): + abi, hexcode = deployer.get_contract_data(path) + bytecode = ethutils.decode_hex(hexcode) + encoded_args = (ContractTranslator(abi).encode_constructor_arguments(args) if args else b'') + code = bytecode + encoded_args + address = ethtester.chain.tx(sender=sender, to=b'', startgas=START_GAS, data=code) + return ethtester.ABIContract(ethtester.chain, abi, address) + return create_contract + + +@pytest.fixture +def root_chain(ethtester, get_contract): + contract = get_contract('RootChain') + ethtester.chain.mine() + return contract diff --git a/tests/contracts/root_chain/test_commit_plasma_block_root.py b/tests/contracts/root_chain/test_commit_plasma_block_root.py new file mode 100644 index 0000000..2bcf00c --- /dev/null +++ b/tests/contracts/root_chain/test_commit_plasma_block_root.py @@ -0,0 +1,27 @@ +import pytest +from ethereum.tools.tester import TransactionFailed +from plasma_core.constants import NULL_HASH + + +def test_commit_plasma_block_root_should_succeed(root_chain, ethtester, ethutils): + random_hash = ethutils.sha3('abc123') + operator = ethtester.accounts[0] + root_chain.commitPlasmaBlockRoot(random_hash, sender=operator.key) + + plasma_block_root = root_chain.plasmaBlockRoots(0) + assert plasma_block_root[0] == random_hash + assert plasma_block_root[1] == ethtester.chain.head_state.timestamp + assert root_chain.currentPlasmaBlockNumber() == 1 + + +def test_commit_plasma_block_root_not_operator_should_fail(root_chain, ethtester, ethutils): + random_hash = ethutils.sha3('abc123') + non_operator = ethtester.accounts[1] + + with pytest.raises(TransactionFailed): + root_chain.commitPlasmaBlockRoot(random_hash, sender=non_operator.key) + + plasma_block_root = root_chain.plasmaBlockRoots(0) + assert plasma_block_root[0] == NULL_HASH + assert plasma_block_root[1] == 0 + assert root_chain.currentPlasmaBlockNumber() == 0