Repository: ethereum/consensus-specs

Issue:
Move deposit contract back to this repo
Deposit contract was moved to another [repo](https://github.com/ethereum/deposit_contract) because we wanted to make eth2.0-specs pure with just documents. But after some recent updates (thanks to @protolambda and @djrtwo), now `eth2.0-specs` repo is the CI/test-gen center. It seems moving deposit contract back can make the spec-syncing and release easier.


Discussion:
Initially (~month ago) I was looking for solutions to have the generators/tests live elsewhere. They are just too dependent/close with the spec itself, so it made sense to go for a more monorepo-ish approach. We could take it further, and just call this the "eth2" monorepo, with release & unified testing. I'm ok with more things being merged into this repo, but we should also look into alternatives, if there are any. And the net benefits of course.

So, to iterate, current options (with their individual pros/cons) are:

## deposit contract in external repo
Pros:
- separate Circle CI
- consistent with structure of other contract-only repositories
- separate documentation dependencies. You use the `pandoc` tool in the contract repo (?)

Cons:
- needs work/time to get right
- possibly needs new documentation
- a PR label probably helps a lot, but it could be considered "noisy" by some

## merge into this repo
Pros:
- monorepo grows, easy to cross-reference things and update things together. (Do we have examples of this from when the deposit contract was hosted here?)
- could integrate it into testing. E.g. local test net + executable spec + simulation -> test behavior

Cons:
- not everyone is a fan of monorepos. Agree in some way, definitely need to keep things organized

## Git-submodule one in the other
Not tried before, not sure about net-benefits, not likely to work well.

## alternatives?
Suggestions welcome.


As a spec consumer, my preference would be for a mono-repo as it would make it easier to locate things and easier to create issues. Additionally, when adding new components its much easier to notice a new dir in an existing repo than a whole new repo.

In my experience, when you're dealing with a big, inter-connected project addressing "issues/PRs clutter" by splitting into separate repos means you have just as many issues/PRs but they're now harder to locate ("which repo was that in again?") or in the wrong place ("this issue should really be on repo X").

Monorepo preference is obviously subjective, just adding my thoughts.
I'm pro bringing it back

Resolving patch:
diff --git a/deposit_contract/contracts/__init__.py b/deposit_contract/contracts/__init__.py
new file mode 100644
diff --git a/deposit_contract/contracts/validator_registration.v.py b/deposit_contract/contracts/validator_registration.v.py
new file mode 100644
--- /dev/null
+++ b/deposit_contract/contracts/validator_registration.v.py
@@ -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
diff --git a/deposit_contract/tool/compile_deposit_contract.py b/deposit_contract/tool/compile_deposit_contract.py
new file mode 100644
--- /dev/null
+++ b/deposit_contract/tool/compile_deposit_contract.py
@@ -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)


Test patch:
diff --git a/deposit_contract/tests/__init__.py b/deposit_contract/tests/__init__.py
new file mode 100644
diff --git a/deposit_contract/tests/contracts/__init__.py b/deposit_contract/tests/contracts/__init__.py
new file mode 100644
diff --git a/deposit_contract/tests/contracts/conftest.py b/deposit_contract/tests/contracts/conftest.py
new file mode 100644
--- /dev/null
+++ b/deposit_contract/tests/contracts/conftest.py
@@ -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
diff --git a/deposit_contract/tests/contracts/test_compile.py b/deposit_contract/tests/contracts/test_compile.py
new file mode 100644
--- /dev/null
+++ b/deposit_contract/tests/contracts/test_compile.py
@@ -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"]
diff --git a/deposit_contract/tests/contracts/test_deposit.py b/deposit_contract/tests/contracts/test_deposit.py
new file mode 100644
--- /dev/null
+++ b/deposit_contract/tests/contracts/test_deposit.py
@@ -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
diff --git a/deposit_contract/tests/contracts/utils.py b/deposit_contract/tests/contracts/utils.py
new file mode 100644
--- /dev/null
+++ b/deposit_contract/tests/contracts/utils.py
@@ -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)