copied from deposit contract

This commit is contained in:
Hsiao-Wei Wang 2019-05-27 13:15:10 +08:00
parent dd091724d0
commit 09f7114b63
No known key found for this signature in database
GPG Key ID: 95B070122902DEA4
13 changed files with 549 additions and 0 deletions

View File

View File

@ -0,0 +1,16 @@
import json
import os
DIR = os.path.dirname(__file__)
def get_deposit_contract_code():
file_path = os.path.join(DIR, './validator_registration.v.py')
deposit_contract_code = open(file_path).read()
return deposit_contract_code
def get_deposit_contract_json():
file_path = os.path.join(DIR, './validator_registration.json')
deposit_contract_json = open(file_path).read()
return json.loads(deposit_contract_json)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,138 @@
MIN_DEPOSIT_AMOUNT: constant(uint256) = 1000000000 # Gwei
FULL_DEPOSIT_AMOUNT: constant(uint256) = 32000000000 # Gwei
CHAIN_START_FULL_DEPOSIT_THRESHOLD: constant(uint256) = 65536 # 2**16
DEPOSIT_CONTRACT_TREE_DEPTH: constant(uint256) = 32
SECONDS_PER_DAY: constant(uint256) = 86400
MAX_64_BIT_VALUE: constant(uint256) = 18446744073709551615 # 2**64 - 1
Deposit: event({
pubkey: bytes[48],
withdrawal_credentials: bytes[32],
amount: bytes[8],
signature: bytes[96],
merkle_tree_index: bytes[8],
})
Eth2Genesis: event({deposit_root: bytes32, deposit_count: bytes[8], time: bytes[8]})
zerohashes: bytes32[DEPOSIT_CONTRACT_TREE_DEPTH]
branch: bytes32[DEPOSIT_CONTRACT_TREE_DEPTH]
deposit_count: uint256
full_deposit_count: uint256
chainStarted: public(bool)
@public
def __init__():
for i in range(DEPOSIT_CONTRACT_TREE_DEPTH - 1):
self.zerohashes[i+1] = sha256(concat(self.zerohashes[i], self.zerohashes[i]))
self.branch[i+1] = self.zerohashes[i + 1]
@public
@constant
def to_little_endian_64(value: uint256) -> bytes[8]:
assert value <= MAX_64_BIT_VALUE
# array access for bytes[] not currently supported in vyper so
# reversing bytes using bitwise uint256 manipulations
y: uint256 = 0
x: uint256 = value
for i in range(8):
y = shift(y, 8)
y = y + bitwise_and(x, 255)
x = shift(x, -8)
return slice(convert(y, bytes32), start=24, len=8)
@public
@constant
def from_little_endian_64(value: bytes[8]) -> uint256:
y: uint256 = 0
x: uint256 = convert(value, uint256)
for i in range(8):
y = y + shift(bitwise_and(x, 255), 8 * (7-i))
x = shift(x, -8)
return y
@public
@constant
def get_deposit_root() -> bytes32:
root: bytes32 = 0x0000000000000000000000000000000000000000000000000000000000000000
size: uint256 = self.deposit_count
for h in range(DEPOSIT_CONTRACT_TREE_DEPTH):
if bitwise_and(size, 1) == 1:
root = sha256(concat(self.branch[h], root))
else:
root = sha256(concat(root, self.zerohashes[h]))
size /= 2
return root
@public
@constant
def get_deposit_count() -> bytes[8]:
return self.to_little_endian_64(self.deposit_count)
@payable
@public
def deposit(pubkey: bytes[48], withdrawal_credentials: bytes[32], signature: bytes[96]):
deposit_amount: uint256 = msg.value / as_wei_value(1, "gwei")
assert deposit_amount >= MIN_DEPOSIT_AMOUNT
amount: bytes[8] = self.to_little_endian_64(deposit_amount)
index: uint256 = self.deposit_count
# add deposit to merkle tree
i: int128 = 0
power_of_two: uint256 = 2
for _ in range(DEPOSIT_CONTRACT_TREE_DEPTH):
if (index+1) % power_of_two != 0:
break
i += 1
power_of_two *= 2
zero_bytes_32: bytes32
pubkey_root: bytes32 = sha256(concat(pubkey, slice(zero_bytes_32, start=0, len=16)))
signature_root: bytes32 = sha256(concat(
sha256(slice(signature, start=0, len=64)),
sha256(concat(slice(signature, start=64, len=32), zero_bytes_32))
))
value: bytes32 = sha256(concat(
sha256(concat(pubkey_root, withdrawal_credentials)),
sha256(concat(
amount,
slice(zero_bytes_32, start=0, len=24),
signature_root,
))
))
for j in range(DEPOSIT_CONTRACT_TREE_DEPTH):
if j < i:
value = sha256(concat(self.branch[j], value))
else:
break
self.branch[i] = value
self.deposit_count += 1
new_deposit_root: bytes32 = self.get_deposit_root()
log.Deposit(
pubkey,
withdrawal_credentials,
amount,
signature,
self.to_little_endian_64(index),
)
if deposit_amount >= FULL_DEPOSIT_AMOUNT:
self.full_deposit_count += 1
if self.full_deposit_count == CHAIN_START_FULL_DEPOSIT_THRESHOLD:
timestamp_day_boundary: uint256 = (
as_unitless_number(block.timestamp) -
as_unitless_number(block.timestamp) % SECONDS_PER_DAY +
2 * SECONDS_PER_DAY
)
log.Eth2Genesis(new_deposit_root,
self.to_little_endian_64(self.deposit_count),
self.to_little_endian_64(timestamp_day_boundary))
self.chainStarted = True

View File

@ -0,0 +1,7 @@
pytest>=3.6,<3.7
tox==3.0.0,
eth-tester[py-evm]==0.1.0b29,
vyper==0.1.0b9,
web3==4.8.3,
pytest==3.6.1,
../../test_libs/pyspec

View File

View File

@ -0,0 +1,111 @@
from random import (
randint,
)
import re
import pytest
from deposit_contract.contracts.utils import (
get_deposit_contract_code,
get_deposit_contract_json,
)
import eth_tester
from eth_tester import (
EthereumTester,
PyEVMBackend,
)
from vyper import (
compiler,
)
from web3 import Web3
from web3.providers.eth_tester import (
EthereumTesterProvider,
)
# Constants
MIN_DEPOSIT_AMOUNT = 1000000000 # Gwei
FULL_DEPOSIT_AMOUNT = 32000000000 # Gwei
CHAIN_START_FULL_DEPOSIT_THRESHOLD = 65536 # 2**16
DEPOSIT_CONTRACT_TREE_DEPTH = 32
TWO_TO_POWER_OF_TREE_DEPTH = 2**DEPOSIT_CONTRACT_TREE_DEPTH
@pytest.fixture
def tester():
return EthereumTester(PyEVMBackend())
@pytest.fixture
def a0(tester):
return tester.get_accounts()[0]
@pytest.fixture
def w3(tester):
web3 = Web3(EthereumTesterProvider(tester))
return web3
@pytest.fixture
def registration_contract(w3, tester):
contract_bytecode = get_deposit_contract_json()['bytecode']
contract_abi = get_deposit_contract_json()['abi']
registration = w3.eth.contract(
abi=contract_abi,
bytecode=contract_bytecode)
tx_hash = registration.constructor().transact()
tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash)
registration_deployed = w3.eth.contract(
address=tx_receipt.contractAddress,
abi=contract_abi
)
return registration_deployed
@pytest.fixture(scope="session")
def chain_start_full_deposit_thresholds():
return [randint(1, 5), randint(6, 10), randint(11, 15)]
@pytest.fixture(params=[0, 1, 2])
def modified_registration_contract(
request,
w3,
tester,
chain_start_full_deposit_thresholds):
# Set CHAIN_START_FULL_DEPOSIT_THRESHOLD to different threshold t
registration_code = get_deposit_contract_code()
t = str(chain_start_full_deposit_thresholds[request.param])
modified_registration_code = re.sub(
r'CHAIN_START_FULL_DEPOSIT_THRESHOLD: constant\(uint256\) = [0-9]+',
'CHAIN_START_FULL_DEPOSIT_THRESHOLD: constant(uint256) = ' + t,
registration_code,
)
assert modified_registration_code != registration_code
contract_bytecode = compiler.compile_code(modified_registration_code)['bytecode']
contract_abi = compiler.mk_full_signature(modified_registration_code)
registration = w3.eth.contract(
abi=contract_abi,
bytecode=contract_bytecode)
tx_hash = registration.constructor().transact()
tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash)
registration_deployed = w3.eth.contract(
address=tx_receipt.contractAddress,
abi=contract_abi
)
setattr(
registration_deployed,
'chain_start_full_deposit_threshold',
chain_start_full_deposit_thresholds[request.param]
)
return registration_deployed
@pytest.fixture
def assert_tx_failed(tester):
def assert_tx_failed(function_to_test, exception=eth_tester.exceptions.TransactionFailed):
snapshot_id = tester.take_snapshot()
with pytest.raises(exception):
function_to_test()
tester.revert_to_snapshot(snapshot_id)
return assert_tx_failed

View File

@ -0,0 +1,18 @@
from deposit_contract.contracts.utils import (
get_deposit_contract_code,
get_deposit_contract_json,
)
from vyper import (
compiler,
)
def test_compile_deposit_contract():
compiled_deposit_contract_json = get_deposit_contract_json()
deposit_contract_code = get_deposit_contract_code()
abi = compiler.mk_full_signature(deposit_contract_code)
bytecode = compiler.compile_code(deposit_contract_code)['bytecode']
assert abi == compiled_deposit_contract_json["abi"]
assert bytecode == compiled_deposit_contract_json["bytecode"]

View File

@ -0,0 +1,221 @@
from hashlib import (
sha256,
)
from random import (
randint,
)
import pytest
import eth_utils
from tests.contracts.conftest import (
DEPOSIT_CONTRACT_TREE_DEPTH,
FULL_DEPOSIT_AMOUNT,
MIN_DEPOSIT_AMOUNT,
)
from eth2spec.utils.minimal_ssz import
SSZType,
hash_tree_root,
)
DepositData = SSZType({
# BLS pubkey
'pubkey': 'bytes48',
# Withdrawal credentials
'withdrawal_credentials': 'bytes32',
# Amount in Gwei
'amount': 'uint64',
# Container self-signature
'signature': 'bytes96',
})
def hash(data):
return sha256(data).digest()
def compute_merkle_root(leaf_nodes):
assert len(leaf_nodes) >= 1
empty_node = b'\x00' * 32
child_nodes = leaf_nodes[:]
for _ in range(DEPOSIT_CONTRACT_TREE_DEPTH):
parent_nodes = []
if len(child_nodes) % 2 == 1:
child_nodes.append(empty_node)
for j in range(0, len(child_nodes), 2):
parent_nodes.append(hash(child_nodes[j] + child_nodes[j + 1]))
child_nodes = parent_nodes
empty_node = hash(empty_node + empty_node)
return child_nodes[0]
@pytest.fixture
def deposit_input():
"""
pubkey: bytes[48]
withdrawal_credentials: bytes[32]
signature: bytes[96]
"""
return (
b'\x11' * 48,
b'\x22' * 32,
b'\x33' * 96,
)
@pytest.mark.parametrize(
'value,success',
[
(0, True),
(10, True),
(55555, True),
(2**64 - 1, True),
(2**64, False),
]
)
def test_to_little_endian_64(registration_contract, value, success, assert_tx_failed):
call = registration_contract.functions.to_little_endian_64(value)
if success:
little_endian_64 = call.call()
assert little_endian_64 == (value).to_bytes(8, 'little')
else:
assert_tx_failed(
lambda: call.call()
)
def test_from_little_endian_64(registration_contract, assert_tx_failed):
values = [0, 2**64 - 1] + [randint(1, 2**64 - 2) for _ in range(10)]
for value in values:
call = registration_contract.functions.from_little_endian_64((value).to_bytes(8, 'little'))
assert call.call() == value
@pytest.mark.parametrize(
'success,deposit_amount',
[
(True, FULL_DEPOSIT_AMOUNT),
(True, MIN_DEPOSIT_AMOUNT),
(False, MIN_DEPOSIT_AMOUNT - 1),
(True, FULL_DEPOSIT_AMOUNT + 1)
]
)
def test_deposit_amount(registration_contract,
w3,
success,
deposit_amount,
assert_tx_failed,
deposit_input):
call = registration_contract.functions.deposit(*deposit_input)
if success:
assert call.transact({"value": deposit_amount * eth_utils.denoms.gwei})
else:
assert_tx_failed(
lambda: call.transact({"value": deposit_amount * eth_utils.denoms.gwei})
)
def test_deposit_log(registration_contract, a0, w3, deposit_input):
log_filter = registration_contract.events.Deposit.createFilter(
fromBlock='latest',
)
deposit_amount_list = [randint(MIN_DEPOSIT_AMOUNT, FULL_DEPOSIT_AMOUNT * 2) for _ in range(3)]
for i in range(3):
registration_contract.functions.deposit(
*deposit_input,
).transact({"value": deposit_amount_list[i] * eth_utils.denoms.gwei})
logs = log_filter.get_new_entries()
assert len(logs) == 1
log = logs[0]['args']
assert log['pubkey'] == deposit_input[0]
assert log['withdrawal_credentials'] == deposit_input[1]
assert log['amount'] == deposit_amount_list[i].to_bytes(8, 'little')
assert log['signature'] == deposit_input[2]
assert log['merkle_tree_index'] == i.to_bytes(8, 'little')
def test_deposit_tree(registration_contract, w3, assert_tx_failed, deposit_input):
log_filter = registration_contract.events.Deposit.createFilter(
fromBlock='latest',
)
deposit_amount_list = [randint(MIN_DEPOSIT_AMOUNT, FULL_DEPOSIT_AMOUNT * 2) for _ in range(10)]
leaf_nodes = []
for i in range(0, 10):
tx_hash = registration_contract.functions.deposit(
*deposit_input,
).transact({"value": deposit_amount_list[i] * eth_utils.denoms.gwei})
receipt = w3.eth.getTransactionReceipt(tx_hash)
print("deposit transaction consumes %d gas" % receipt['gasUsed'])
logs = log_filter.get_new_entries()
assert len(logs) == 1
log = logs[0]['args']
assert log["merkle_tree_index"] == i.to_bytes(8, 'little')
deposit_data = DepositData(
pubkey=deposit_input[0][:20],
withdrawal_credentials=deposit_input[1],
amount=deposit_amount_list[i],
signature=deposit_input[2],
)
hash_tree_root_result = hash_tree_root(deposit_data)
leaf_nodes.append(hash_tree_root_result)
root = compute_merkle_root(leaf_nodes)
assert root == registration_contract.functions.get_deposit_root().call()
def test_chain_start(modified_registration_contract, w3, assert_tx_failed, deposit_input):
t = getattr(modified_registration_contract, 'chain_start_full_deposit_threshold')
# CHAIN_START_FULL_DEPOSIT_THRESHOLD is set to t
min_deposit_amount = MIN_DEPOSIT_AMOUNT * eth_utils.denoms.gwei # in wei
full_deposit_amount = FULL_DEPOSIT_AMOUNT * eth_utils.denoms.gwei
log_filter = modified_registration_contract.events.Eth2Genesis.createFilter(
fromBlock='latest',
)
index_not_full_deposit = randint(0, t - 1)
for i in range(t):
if i == index_not_full_deposit:
# Deposit with value below FULL_DEPOSIT_AMOUNT
modified_registration_contract.functions.deposit(
*deposit_input,
).transact({"value": min_deposit_amount})
logs = log_filter.get_new_entries()
# Eth2Genesis event should not be triggered
assert len(logs) == 0
else:
# Deposit with value FULL_DEPOSIT_AMOUNT
modified_registration_contract.functions.deposit(
*deposit_input,
).transact({"value": full_deposit_amount})
logs = log_filter.get_new_entries()
# Eth2Genesis event should not be triggered
assert len(logs) == 0
# Make 1 more deposit with value FULL_DEPOSIT_AMOUNT to trigger Eth2Genesis event
modified_registration_contract.functions.deposit(
*deposit_input,
).transact({"value": full_deposit_amount})
logs = log_filter.get_new_entries()
assert len(logs) == 1
timestamp = int(w3.eth.getBlock(w3.eth.blockNumber)['timestamp'])
timestamp_day_boundary = timestamp + (86400 - timestamp % 86400) + 86400
log = logs[0]['args']
assert log['deposit_root'] == modified_registration_contract.functions.get_deposit_root().call()
assert int.from_bytes(log['time'], byteorder='little') == timestamp_day_boundary
assert modified_registration_contract.functions.chainStarted().call() is True
# Make 1 deposit with value FULL_DEPOSIT_AMOUNT and
# check that Eth2Genesis event is not triggered
modified_registration_contract.functions.deposit(
*deposit_input,
).transact({"value": full_deposit_amount})
logs = log_filter.get_new_entries()
assert len(logs) == 0

View File

View File

@ -0,0 +1,4 @@
def test_import():
import deposit_contract # noqa: F401

View File

@ -0,0 +1,33 @@
import argparse
import json
import os
from vyper import (
compiler,
)
DIR = os.path.dirname(__file__)
def generate_compiled_json(file_path: str):
deposit_contract_code = open(file_path).read()
abi = compiler.mk_full_signature(deposit_contract_code)
bytecode = compiler.compile_code(deposit_contract_code)['bytecode']
contract_json = {
'abi': abi,
'bytecode': bytecode,
}
# write json
basename = os.path.basename(file_path)
dirname = os.path.dirname(file_path)
contract_name = basename.split('.')[0]
with open(dirname + "/{}.json".format(contract_name), 'w') as f_write:
json.dump(contract_json, f_write)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("path", type=str, help="the path of the contract")
args = parser.parse_args()
path = args.path
generate_compiled_json(path)