Merge pull request #1127 from ethereum/deposit_contract
Move deposit contract back
This commit is contained in:
commit
6f82480df2
|
@ -1,7 +1,7 @@
|
||||||
version: 2.1
|
version: 2.1
|
||||||
commands:
|
commands:
|
||||||
restore_cached_venv:
|
restore_cached_venv:
|
||||||
description: "Restores a cached venv"
|
description: "Restore a cached venv"
|
||||||
parameters:
|
parameters:
|
||||||
reqs_checksum:
|
reqs_checksum:
|
||||||
type: string
|
type: string
|
||||||
|
@ -16,7 +16,7 @@ commands:
|
||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- << parameters.venv_name >>-venv-
|
- << parameters.venv_name >>-venv-
|
||||||
save_cached_venv:
|
save_cached_venv:
|
||||||
description: "Saves a venv into a cache"
|
description: "Save a venv into a cache"
|
||||||
parameters:
|
parameters:
|
||||||
reqs_checksum:
|
reqs_checksum:
|
||||||
type: string
|
type: string
|
||||||
|
@ -31,6 +31,32 @@ commands:
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: << parameters.venv_name >>-venv-<< parameters.reqs_checksum >>
|
key: << parameters.venv_name >>-venv-<< parameters.reqs_checksum >>
|
||||||
paths: << parameters.venv_path >>
|
paths: << parameters.venv_path >>
|
||||||
|
restore_pyspec_cached_venv:
|
||||||
|
description: "Restore the cache with pyspec keys"
|
||||||
|
steps:
|
||||||
|
- restore_cached_venv:
|
||||||
|
venv_name: v2-pyspec
|
||||||
|
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}
|
||||||
|
save_pyspec_cached_venv:
|
||||||
|
description: Save a venv into a cache with pyspec keys"
|
||||||
|
steps:
|
||||||
|
- save_cached_venv:
|
||||||
|
venv_name: v2-pyspec
|
||||||
|
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}
|
||||||
|
venv_path: ./test_libs/pyspec/venv
|
||||||
|
restore_deposit_contract_cached_venv:
|
||||||
|
description: "Restore the cache with deposit_contract keys"
|
||||||
|
steps:
|
||||||
|
- restore_cached_venv:
|
||||||
|
venv_name: v4-deposit-contract
|
||||||
|
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "deposit_contract/requirements-testing.txt" }}
|
||||||
|
save_deposit_contract_cached_venv:
|
||||||
|
description: Save a venv into a cache with deposit_contract keys"
|
||||||
|
steps:
|
||||||
|
- save_cached_venv:
|
||||||
|
venv_name: v4-deposit-contract
|
||||||
|
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "deposit_contract/requirements-testing.txt" }}
|
||||||
|
venv_path: ./deposit_contract/venv
|
||||||
jobs:
|
jobs:
|
||||||
checkout_specs:
|
checkout_specs:
|
||||||
docker:
|
docker:
|
||||||
|
@ -52,23 +78,18 @@ jobs:
|
||||||
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
||||||
paths:
|
paths:
|
||||||
- ~/specs-repo
|
- ~/specs-repo
|
||||||
install_env:
|
install_pyspec_test:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/python:3.6
|
- image: circleci/python:3.6
|
||||||
working_directory: ~/specs-repo
|
working_directory: ~/specs-repo
|
||||||
steps:
|
steps:
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
||||||
- restore_cached_venv:
|
- restore_pyspec_cached_venv
|
||||||
venv_name: v2-pyspec
|
|
||||||
reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}'
|
|
||||||
- run:
|
- run:
|
||||||
name: Install pyspec requirements
|
name: Install pyspec requirements
|
||||||
command: make install_test
|
command: make install_test
|
||||||
- save_cached_venv:
|
- save_pyspec_cached_venv
|
||||||
venv_name: v2-pyspec
|
|
||||||
reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}'
|
|
||||||
venv_path: ./test_libs/pyspec/venv
|
|
||||||
test:
|
test:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/python:3.6
|
- image: circleci/python:3.6
|
||||||
|
@ -76,9 +97,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
||||||
- restore_cached_venv:
|
- restore_pyspec_cached_venv
|
||||||
venv_name: v2-pyspec
|
|
||||||
reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}'
|
|
||||||
- run:
|
- run:
|
||||||
name: Run py-tests
|
name: Run py-tests
|
||||||
command: make citest
|
command: make citest
|
||||||
|
@ -91,23 +110,50 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
||||||
- restore_cached_venv:
|
- restore_pyspec_cached_venv
|
||||||
venv_name: v2-pyspec
|
|
||||||
reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}'
|
|
||||||
- run:
|
- run:
|
||||||
name: Run linter
|
name: Run linter
|
||||||
command: make lint
|
command: make lint
|
||||||
|
install_deposit_contract_test:
|
||||||
|
docker:
|
||||||
|
- image: circleci/python:3.6
|
||||||
|
working_directory: ~/specs-repo
|
||||||
|
steps:
|
||||||
|
- restore_cache:
|
||||||
|
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
||||||
|
- restore_deposit_contract_cached_venv
|
||||||
|
- run:
|
||||||
|
name: Install deposit contract requirements
|
||||||
|
command: make install_deposit_contract_test
|
||||||
|
- save_deposit_contract_cached_venv
|
||||||
|
deposit_contract:
|
||||||
|
docker:
|
||||||
|
- image: circleci/python:3.6
|
||||||
|
working_directory: ~/specs-repo
|
||||||
|
steps:
|
||||||
|
- restore_cache:
|
||||||
|
key: v1-specs-repo-{{ .Branch }}-{{ .Revision }}
|
||||||
|
- restore_deposit_contract_cached_venv
|
||||||
|
- run:
|
||||||
|
name: Run deposit contract test
|
||||||
|
command: make test_deposit_contract
|
||||||
workflows:
|
workflows:
|
||||||
version: 2.1
|
version: 2.1
|
||||||
test_spec:
|
test_spec:
|
||||||
jobs:
|
jobs:
|
||||||
- checkout_specs
|
- checkout_specs
|
||||||
- install_env:
|
- install_pyspec_test:
|
||||||
requires:
|
requires:
|
||||||
- checkout_specs
|
- checkout_specs
|
||||||
- test:
|
- test:
|
||||||
requires:
|
requires:
|
||||||
- install_env
|
- install_pyspec_test
|
||||||
- lint:
|
- lint:
|
||||||
requires:
|
requires:
|
||||||
- test
|
- test
|
||||||
|
- install_deposit_contract_test:
|
||||||
|
requires:
|
||||||
|
- checkout_specs
|
||||||
|
- deposit_contract:
|
||||||
|
requires:
|
||||||
|
- install_deposit_contract_test
|
||||||
|
|
15
Makefile
15
Makefile
|
@ -4,6 +4,7 @@ TEST_LIBS_DIR = ./test_libs
|
||||||
PY_SPEC_DIR = $(TEST_LIBS_DIR)/pyspec
|
PY_SPEC_DIR = $(TEST_LIBS_DIR)/pyspec
|
||||||
YAML_TEST_DIR = ./eth2.0-spec-tests/tests
|
YAML_TEST_DIR = ./eth2.0-spec-tests/tests
|
||||||
GENERATOR_DIR = ./test_generators
|
GENERATOR_DIR = ./test_generators
|
||||||
|
DEPOSIT_CONTRACT_DIR = ./deposit_contract
|
||||||
CONFIGS_DIR = ./configs
|
CONFIGS_DIR = ./configs
|
||||||
|
|
||||||
# Collect a list of generator names
|
# Collect a list of generator names
|
||||||
|
@ -21,7 +22,7 @@ PY_SPEC_PHASE_1_DEPS = $(SPEC_DIR)/core/1_*.md
|
||||||
PY_SPEC_ALL_TARGETS = $(PY_SPEC_PHASE_0_TARGETS) $(PY_SPEC_PHASE_1_TARGETS)
|
PY_SPEC_ALL_TARGETS = $(PY_SPEC_PHASE_0_TARGETS) $(PY_SPEC_PHASE_1_TARGETS)
|
||||||
|
|
||||||
|
|
||||||
.PHONY: clean all test citest gen_yaml_tests pyspec phase0 phase1 install_test
|
.PHONY: clean all test citest lint gen_yaml_tests pyspec phase0 phase1 install_test install_deposit_contract_test test_deposit_contract compile_deposit_contract
|
||||||
|
|
||||||
all: $(PY_SPEC_ALL_TARGETS) $(YAML_TEST_DIR) $(YAML_TEST_TARGETS)
|
all: $(PY_SPEC_ALL_TARGETS) $(YAML_TEST_DIR) $(YAML_TEST_TARGETS)
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ clean:
|
||||||
rm -rf $(GENERATOR_VENVS)
|
rm -rf $(GENERATOR_VENVS)
|
||||||
rm -rf $(PY_SPEC_DIR)/venv $(PY_SPEC_DIR)/.pytest_cache
|
rm -rf $(PY_SPEC_DIR)/venv $(PY_SPEC_DIR)/.pytest_cache
|
||||||
rm -rf $(PY_SPEC_ALL_TARGETS)
|
rm -rf $(PY_SPEC_ALL_TARGETS)
|
||||||
|
rm -rf $(DEPOSIT_CONTRACT_DIR)/venv $(DEPOSIT_CONTRACT_DIR)/.pytest_cache
|
||||||
|
|
||||||
# "make gen_yaml_tests" to run generators
|
# "make gen_yaml_tests" to run generators
|
||||||
gen_yaml_tests: $(PY_SPEC_ALL_TARGETS) $(YAML_TEST_TARGETS)
|
gen_yaml_tests: $(PY_SPEC_ALL_TARGETS) $(YAML_TEST_TARGETS)
|
||||||
|
@ -49,6 +51,17 @@ lint: $(PY_SPEC_ALL_TARGETS)
|
||||||
cd $(PY_SPEC_DIR); . venv/bin/activate; \
|
cd $(PY_SPEC_DIR); . venv/bin/activate; \
|
||||||
flake8 --ignore=E252,W504,W503 --max-line-length=120 ./eth2spec;
|
flake8 --ignore=E252,W504,W503 --max-line-length=120 ./eth2spec;
|
||||||
|
|
||||||
|
install_deposit_contract_test: $(PY_SPEC_ALL_TARGETS)
|
||||||
|
cd $(DEPOSIT_CONTRACT_DIR); python3 -m venv venv; . venv/bin/activate; pip3 install -r requirements-testing.txt
|
||||||
|
|
||||||
|
compile_deposit_contract:
|
||||||
|
cd $(DEPOSIT_CONTRACT_DIR); . venv/bin/activate; \
|
||||||
|
python tool/compile_deposit_contract.py contracts/validator_registration.v.py;
|
||||||
|
|
||||||
|
test_deposit_contract:
|
||||||
|
cd $(DEPOSIT_CONTRACT_DIR); . venv/bin/activate; \
|
||||||
|
python -m pytest .
|
||||||
|
|
||||||
# "make pyspec" to create the pyspec for all phases.
|
# "make pyspec" to create the pyspec for all phases.
|
||||||
pyspec: $(PY_SPEC_ALL_TARGETS)
|
pyspec: $(PY_SPEC_ALL_TARGETS)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Deposit contract
|
||||||
|
|
||||||
|
## How to set up the testing environment?
|
||||||
|
|
||||||
|
Under the `eth2.0-specs` directory, execute:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make install_deposit_contract_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to compile the contract?
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make compile_deposit_contract
|
||||||
|
```
|
||||||
|
|
||||||
|
The ABI and bytecode will be updated at [`contracts/validator_registration.json`](./contracts/validator_registration.json).
|
||||||
|
|
||||||
|
|
||||||
|
## How to run tests?
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make test_deposit_contract
|
||||||
|
```
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,140 @@
|
||||||
|
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
|
||||||
|
PUBKEY_LENGTH: constant(uint256) = 48 # bytes
|
||||||
|
WITHDRAWAL_CREDENTIALS_LENGTH: constant(uint256) = 32 # bytes
|
||||||
|
SIGNATURE_LENGTH: constant(uint256) = 96 # bytes
|
||||||
|
MAX_DEPOSIT_COUNT: constant(uint256) = 4294967295 # 2**DEPOSIT_CONTRACT_TREE_DEPTH - 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]))
|
||||||
|
|
||||||
|
|
||||||
|
@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 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[PUBKEY_LENGTH],
|
||||||
|
withdrawal_credentials: bytes[WITHDRAWAL_CREDENTIALS_LENGTH],
|
||||||
|
signature: bytes[SIGNATURE_LENGTH]):
|
||||||
|
# Prevent edge case in computing `self.branch` when `self.deposit_count == MAX_DEPOSIT_COUNT`
|
||||||
|
# NOTE: reaching this point with the constants as currently defined is impossible due to the
|
||||||
|
# uni-directional nature of transfers from eth1 to eth2 and the total ether supply (< 130M).
|
||||||
|
assert self.deposit_count < MAX_DEPOSIT_COUNT
|
||||||
|
|
||||||
|
assert len(pubkey) == PUBKEY_LENGTH
|
||||||
|
assert len(withdrawal_credentials) == WITHDRAWAL_CREDENTIALS_LENGTH
|
||||||
|
assert len(signature) == SIGNATURE_LENGTH
|
||||||
|
|
||||||
|
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
|
||||||
|
size: uint256 = index + 1
|
||||||
|
for _ in range(DEPOSIT_CONTRACT_TREE_DEPTH):
|
||||||
|
if bitwise_and(size, 1) == 1:
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
size /= 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
|
||||||
|
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
|
||||||
|
)
|
||||||
|
new_deposit_root: bytes32 = self.get_deposit_root()
|
||||||
|
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,5 @@
|
||||||
|
eth-tester[py-evm]==0.1.0b39
|
||||||
|
vyper==0.1.0b9
|
||||||
|
web3==5.0.0b2
|
||||||
|
pytest==3.6.1
|
||||||
|
../test_libs/pyspec
|
|
@ -0,0 +1,112 @@
|
||||||
|
from random import (
|
||||||
|
randint,
|
||||||
|
)
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import eth_tester
|
||||||
|
from eth_tester import (
|
||||||
|
EthereumTester,
|
||||||
|
PyEVMBackend,
|
||||||
|
)
|
||||||
|
from vyper import (
|
||||||
|
compiler,
|
||||||
|
)
|
||||||
|
from web3 import Web3
|
||||||
|
from web3.providers.eth_tester import (
|
||||||
|
EthereumTesterProvider,
|
||||||
|
)
|
||||||
|
from .utils import (
|
||||||
|
get_deposit_contract_code,
|
||||||
|
get_deposit_contract_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 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,19 @@
|
||||||
|
from vyper import (
|
||||||
|
compiler,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .utils import (
|
||||||
|
get_deposit_contract_code,
|
||||||
|
get_deposit_contract_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,236 @@
|
||||||
|
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.phase0.spec import (
|
||||||
|
DepositData,
|
||||||
|
)
|
||||||
|
from eth2spec.utils.hash_function import hash
|
||||||
|
from eth2spec.utils.ssz.ssz_impl import (
|
||||||
|
hash_tree_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'invalid_pubkey,invalid_withdrawal_credentials,invalid_signature,success',
|
||||||
|
[
|
||||||
|
(False, False, False, True),
|
||||||
|
(True, False, False, False),
|
||||||
|
(False, True, False, False),
|
||||||
|
(False, False, True, False),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_deposit_inputs(registration_contract,
|
||||||
|
w3,
|
||||||
|
assert_tx_failed,
|
||||||
|
deposit_input,
|
||||||
|
invalid_pubkey,
|
||||||
|
invalid_withdrawal_credentials,
|
||||||
|
invalid_signature,
|
||||||
|
success):
|
||||||
|
pubkey = deposit_input[0][2:] if invalid_pubkey else deposit_input[0]
|
||||||
|
if invalid_withdrawal_credentials: # this one is different to satisfy linter
|
||||||
|
withdrawal_credentials = deposit_input[1][2:]
|
||||||
|
else:
|
||||||
|
withdrawal_credentials = deposit_input[1]
|
||||||
|
signature = deposit_input[2][2:] if invalid_signature else deposit_input[2]
|
||||||
|
|
||||||
|
call = registration_contract.functions.deposit(
|
||||||
|
pubkey,
|
||||||
|
withdrawal_credentials,
|
||||||
|
signature,
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
assert call.transact({"value": FULL_DEPOSIT_AMOUNT * eth_utils.denoms.gwei})
|
||||||
|
else:
|
||||||
|
assert_tx_failed(
|
||||||
|
lambda: call.transact({"value": FULL_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],
|
||||||
|
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,16 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
DIR = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_deposit_contract_code():
|
||||||
|
file_path = os.path.join(DIR, './../../contracts/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, './../../contracts/validator_registration.json')
|
||||||
|
deposit_contract_json = open(file_path).read()
|
||||||
|
return json.loads(deposit_contract_json)
|
|
@ -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