Skip to content

Commit

Permalink
evm: Support contracts in functional tests (#2023)
Browse files Browse the repository at this point in the history
* Add contract compilation and interaction to Python tests

* Fix path

* Clean up test

* Change function name

* Refactor to EVMProvider and KeyPair

* Move files to test_framework

* Add EVMProvider to TestNode

* Add static from_node method to KeyPair

* Add static from_file method to EVMContract

* Add static from_node method to EVMProvider

* Remove web3 checksum
  • Loading branch information
shohamc1 authored Jun 6, 2023
1 parent 531c5f0 commit 571ae53
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 0 deletions.
15 changes: 15 additions & 0 deletions test/functional/contracts/SimpleStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Test {
uint256 number;

function store(uint256 num) public {
number = num;
}

function retrieve() public view returns (uint256) {
return number;
}
}
58 changes: 58 additions & 0 deletions test/functional/feature_evm_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2019 The Bitcoin Core developers
# Copyright (c) DeFi Blockchain Developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""Test EVM contract"""

from test_framework.util import assert_equal
from test_framework.test_framework import DefiTestFramework
from test_framework.evm_contract import EVMContract
from test_framework.evm_key_pair import KeyPair


class EVMTest(DefiTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
self.extra_args = [
['-dummypos=0', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=51', '-eunosheight=80',
'-fortcanningheight=82', '-fortcanninghillheight=84', '-fortcanningroadheight=86',
'-fortcanningcrunchheight=88', '-fortcanningspringheight=90', '-fortcanninggreatworldheight=94',
'-fortcanningepilogueheight=96', '-grandcentralheight=101', '-nextnetworkupgradeheight=105',
'-subsidytest=1', '-txindex=1'],
]

def setup(self):
self.address = self.nodes[0].get_genesis_keys().ownerAuthAddress

# Generate chain
self.nodes[0].generate(105)

self.nodes[0].getbalance()
self.nodes[0].utxostoaccount({self.address: "201@DFI"})
self.nodes[0].setgov({"ATTRIBUTES": {'v0/params/feature/evm': 'true'}})
self.nodes[0].generate(1)

def run_test(self):
node = self.nodes[0]
self.setup()

key_pair = KeyPair.from_node(node)
address = key_pair.address

node.transferdomain(1, {self.address: ["50@DFI"]}, {address: ["50@DFI"]})
node.generate(1)

evm_contract = EVMContract.from_file("SimpleStorage.sol", "Test").compile()
contract = node.evm.deploy_compiled_contract(key_pair, evm_contract)

# set variable
node.evm.sign_and_send(contract.functions.store(10), key_pair)

# get variable
assert_equal(contract.functions.retrieve().call(), 10)


if __name__ == '__main__':
EVMTest().main()
45 changes: 45 additions & 0 deletions test/functional/test_framework/evm_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os
from typing import List, Dict

from solcx import compile_standard


class EVMContract:
# Solidity compiler version
_solc_version = "0.8.10"
_path_prefix = "../contracts"

def __init__(self, code: str, file_name: str, contract_name: str):
self.code = code
self.file_name = file_name
self.contract_name = contract_name

@staticmethod
def from_file(file_name: str, contract_name: str):
with open(f"{os.path.dirname(__file__)}/{EVMContract._path_prefix}/{file_name}", "r") as file:
return EVMContract(file.read(), file_name, contract_name)

def compile(self) -> (List[Dict], str):
compiled_sol = compile_standard(
{
"language": "Solidity",
"sources": {self.file_name: {"content": self.code}},
"settings": {
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
]
}
}
},
},
solc_version=self._solc_version,
)

data = compiled_sol["contracts"][self.file_name][self.contract_name]
abi = data["abi"]
bytecode = data["evm"]["bytecode"]["object"]

return abi, bytecode
28 changes: 28 additions & 0 deletions test/functional/test_framework/evm_key_pair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from eth_account import Account


def validate_keys(pkey, pkey_address):
account = Account.from_key(pkey)

if account.address != pkey_address:
raise RuntimeError(
f"""
Private key does not correspond to provided address.
Address provided: {pkey_address}
Address computed: {account.address}
""")
else:
return pkey, pkey_address


class KeyPair:
def __init__(self, pkey: str = None, pkey_address: str = None):
self.pkey, self.address = validate_keys(pkey, pkey_address)

@staticmethod
def from_node(node):
# get address from node
address = node.getnewaddress("", "eth")
pkey = node.dumpprivkey(address)

return KeyPair(pkey, address)
50 changes: 50 additions & 0 deletions test/functional/test_framework/evm_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Type

from web3 import Web3
from web3.contract import Contract
from web3.contract.contract import ContractFunction

from .evm_key_pair import KeyPair


class EVMProvider:
def __init__(self, rpc_url, generate):
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
self.generator = generate

@staticmethod
def from_node(node):
return EVMProvider(node.get_evm_rpc(), node.generate)

def deploy_compiled_contract(self, signer: KeyPair, compiled_contract, constructor=None) -> Type[Contract]:
abi, bytecode = compiled_contract

nonce = self.w3.eth.get_transaction_count(signer.address)
tx = self.w3.eth.contract(abi=abi, bytecode=bytecode).constructor(constructor).build_transaction({
'chainId': 1133,
'nonce': nonce,
'gasPrice': Web3.to_wei(5, "gwei")
})

signed_tx = self.w3.eth.account.sign_transaction(tx, private_key=signer.pkey)
deploy_tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)

self.generator(1)

tx_receipt = self.w3.eth.wait_for_transaction_receipt(deploy_tx_hash)
return self.w3.eth.contract(address=tx_receipt.contractAddress, abi=abi)

def sign_and_send(self, fn: ContractFunction, signer: KeyPair, gasprice: int = 5):
nonce = self.w3.eth.get_transaction_count(signer.address)
tx = fn.build_transaction({
'chainId': self.w3.eth.chain_id,
'nonce': nonce,
'gasPrice': Web3.to_wei(5, "gwei")
})

signed_tx = self.w3.eth.account.sign_transaction(tx, private_key=signer.pkey)
tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)

self.generator(1)

self.w3.eth.wait_for_transaction_receipt(tx_hash)
7 changes: 7 additions & 0 deletions test/functional/test_framework/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
wait_until,
p2p_port,
)
from .evm_provider import EVMProvider

DEFID_PROC_WAIT_TIMEOUT = 60

Expand Down Expand Up @@ -130,6 +131,8 @@ def __init__(self, i, datadir, *, chain, rpchost, evm_rpchost, timewait, defid,

self.p2ps = []

self.evm = None

MnKeys = collections.namedtuple('MnKeys',
['ownerAuthAddress', 'ownerPrivKey', 'operatorAuthAddress', 'operatorPrivKey'])
PRIV_KEYS = [
Expand Down Expand Up @@ -340,6 +343,7 @@ def wait_for_rpc_connection(self):
self.rpc_connected = True
self.url = self.rpc.url
self.evm_url = self.evm_rpc.url
self.evm = EVMProvider(self.evm_url, self.generate)
return
except IOError as e:
if e.errno != errno.ECONNREFUSED: # Port not yet open?
Expand Down Expand Up @@ -414,6 +418,9 @@ def is_node_stopped(self):
def wait_until_stopped(self, timeout=DEFID_PROC_WAIT_TIMEOUT):
wait_until(self.is_node_stopped, timeout=timeout)

def get_evm_rpc(self) -> str:
return self.evm_url

@contextlib.contextmanager
def assert_debug_log(self, expected_msgs, timeout=2):
time_end = time.time() + timeout
Expand Down

0 comments on commit 571ae53

Please sign in to comment.