copied from deposit contract
This commit is contained in:
parent
dd091724d0
commit
09f7114b63
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"]
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
|
||||
def test_import():
|
||||
import deposit_contract # noqa: F401
|
|
@ -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)
|
Loading…
Reference in New Issue