Skip to content

Commit f7109cd

Browse files
committed
Add eth/beacon/deposits.py
1 parent 9044f99 commit f7109cd

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed

eth/beacon/deposits.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from typing import (
2+
Sequence,
3+
Tuple,
4+
)
5+
6+
from eth_typing import (
7+
Hash32,
8+
)
9+
from eth_utils import (
10+
ValidationError,
11+
)
12+
13+
from eth.utils import bls
14+
15+
from eth.beacon.constants import (
16+
EMPTY_SIGNATURE,
17+
)
18+
from eth.beacon.enums import (
19+
SignatureDomain,
20+
ValidatorStatusCode,
21+
)
22+
from eth.beacon.types.deposit_input import DepositInput
23+
from eth.beacon.types.states import BeaconState
24+
from eth.beacon.types.validator_records import ValidatorRecord
25+
from eth.beacon.helpers import (
26+
get_domain,
27+
)
28+
29+
30+
def min_empty_validator_index(validators: Sequence[ValidatorRecord],
31+
current_slot: int,
32+
zero_balance_validator_ttl: int) -> int:
33+
for index, validator in enumerate(validators):
34+
if (
35+
validator.balance == 0 and
36+
validator.latest_status_change_slot + zero_balance_validator_ttl <=
37+
current_slot
38+
):
39+
return index
40+
return None
41+
42+
43+
def validate_proof_of_possession(state: BeaconState,
44+
pubkey: int,
45+
proof_of_possession: bytes,
46+
withdrawal_credentials: Hash32,
47+
randao_commitment: Hash32) -> bool:
48+
deposit_input = DepositInput(
49+
pubkey=pubkey,
50+
withdrawal_credentials=withdrawal_credentials,
51+
randao_commitment=randao_commitment,
52+
proof_of_possession=EMPTY_SIGNATURE,
53+
)
54+
55+
if not bls.verify(
56+
pubkey=pubkey,
57+
# TODO: change to hash_tree_root(deposit_input) when we have SSZ tree hashing
58+
message=deposit_input.root,
59+
signature=proof_of_possession,
60+
domain=get_domain(
61+
state.fork_data,
62+
state.slot,
63+
SignatureDomain.DOMAIN_DEPOSIT,
64+
)
65+
):
66+
raise ValidationError(
67+
"BLS signature verification error"
68+
)
69+
70+
return True
71+
72+
73+
def process_deposit(state: BeaconState,
74+
pubkey: int,
75+
deposit: int,
76+
proof_of_possession: bytes,
77+
withdrawal_credentials: Hash32,
78+
randao_commitment: Hash32,
79+
zero_balance_validator_ttl: int) -> Tuple[BeaconState, int]:
80+
"""
81+
Process a deposit from Ethereum 1.0.
82+
"""
83+
validate_proof_of_possession(
84+
state,
85+
pubkey,
86+
proof_of_possession,
87+
withdrawal_credentials,
88+
randao_commitment,
89+
)
90+
91+
validator_pubkeys = tuple([v.pubkey for v in state.validator_registry])
92+
if pubkey not in validator_pubkeys:
93+
# Add new validator
94+
validator = ValidatorRecord(
95+
pubkey=pubkey,
96+
withdrawal_credentials=withdrawal_credentials,
97+
randao_commitment=randao_commitment,
98+
randao_layers=0,
99+
balance=deposit,
100+
status=ValidatorStatusCode.PENDING_ACTIVATION,
101+
latest_status_change_slot=state.slot,
102+
exit_count=0,
103+
)
104+
105+
# Check if there's empty validator index
106+
index = min_empty_validator_index(
107+
state.validator_registry,
108+
state.slot,
109+
zero_balance_validator_ttl,
110+
)
111+
if index is None:
112+
# Append to the validator_registry
113+
with state.build_changeset() as state_changeset:
114+
state_changeset.validator_registry = (
115+
state.validator_registry + (validator,)
116+
)
117+
state = state_changeset.commit()
118+
index = len(state.validator_registry) - 1
119+
else:
120+
# Use the empty validator index
121+
state = state.update_validator(index, validator)
122+
else:
123+
# Top-up - increase balance by deposit
124+
index = validator_pubkeys.index(pubkey)
125+
validator = state.validator_registry[index]
126+
127+
if validator.withdrawal_credentials != validator.withdrawal_credentials:
128+
raise ValidationError(
129+
"`withdrawal_credentials` are incorrect:\n"
130+
"\texpected: %s, found: %s" % (
131+
validator.withdrawal_credentials,
132+
validator.withdrawal_credentials,
133+
)
134+
)
135+
136+
# Update validator's balance and state
137+
with validator.build_changeset() as validator_changeset:
138+
validator_changeset.balance = validator.balance + deposit
139+
validator = validator_changeset.commit()
140+
state = state.update_validator(index, validator)
141+
142+
return state, index

tests/beacon/test_deposits.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import pytest
2+
3+
from eth_utils import (
4+
denoms,
5+
ValidationError,
6+
)
7+
8+
from eth.utils import bls
9+
10+
from eth.beacon.constants import (
11+
EMPTY_SIGNATURE,
12+
)
13+
from eth.beacon.enums import (
14+
SignatureDomain,
15+
)
16+
from eth.beacon.helpers import (
17+
get_domain,
18+
)
19+
from eth.beacon.deposits import (
20+
process_deposit,
21+
validate_proof_of_possession,
22+
)
23+
from eth.beacon.types.states import BeaconState
24+
from eth.beacon.types.deposit_input import DepositInput
25+
26+
27+
def sign_proof_of_possession(deposit_input, privkey, domain):
28+
return bls.sign(deposit_input.root, privkey, domain)
29+
30+
31+
def make_deposit_input(pubkey, withdrawal_credentials, randao_commitment):
32+
return DepositInput(
33+
pubkey=pubkey,
34+
withdrawal_credentials=withdrawal_credentials,
35+
randao_commitment=randao_commitment,
36+
proof_of_possession=EMPTY_SIGNATURE,
37+
)
38+
39+
40+
@pytest.mark.parametrize(
41+
"expected",
42+
(
43+
(True),
44+
(ValidationError),
45+
),
46+
)
47+
def test_validate_proof_of_possession(sample_beacon_state_params, pubkeys, privkeys, expected):
48+
state = BeaconState(**sample_beacon_state_params)
49+
50+
privkey = privkeys[0]
51+
pubkey = pubkeys[0]
52+
withdrawal_credentials = b'\x34' * 32
53+
randao_commitment = b'\x56' * 32
54+
domain = get_domain(
55+
state.fork_data,
56+
state.slot,
57+
SignatureDomain.DOMAIN_DEPOSIT,
58+
)
59+
60+
deposit_input = make_deposit_input(
61+
pubkey=pubkey,
62+
withdrawal_credentials=withdrawal_credentials,
63+
randao_commitment=randao_commitment,
64+
)
65+
if expected is True:
66+
proof_of_possession = sign_proof_of_possession(deposit_input, privkey, domain)
67+
68+
assert validate_proof_of_possession(
69+
state=state,
70+
pubkey=pubkey,
71+
proof_of_possession=proof_of_possession,
72+
withdrawal_credentials=withdrawal_credentials,
73+
randao_commitment=randao_commitment,
74+
)
75+
else:
76+
proof_of_possession = b'\x11' * 32
77+
with pytest.raises(ValidationError):
78+
validate_proof_of_possession(
79+
state=state,
80+
pubkey=pubkey,
81+
proof_of_possession=proof_of_possession,
82+
withdrawal_credentials=withdrawal_credentials,
83+
randao_commitment=randao_commitment,
84+
)
85+
86+
87+
def test_process_deposit(sample_beacon_state_params,
88+
zero_balance_validator_ttl,
89+
privkeys,
90+
pubkeys):
91+
state = BeaconState(**sample_beacon_state_params).copy(
92+
slot=zero_balance_validator_ttl + 1,
93+
validator_registry=(),
94+
)
95+
96+
privkey_1 = privkeys[0]
97+
pubkey_1 = pubkeys[0]
98+
deposit = 32 * denoms.gwei
99+
withdrawal_credentials = b'\x34' * 32
100+
randao_commitment = b'\x56' * 32
101+
domain = get_domain(
102+
state.fork_data,
103+
state.slot,
104+
SignatureDomain.DOMAIN_DEPOSIT,
105+
)
106+
107+
deposit_input = make_deposit_input(
108+
pubkey=pubkey_1,
109+
withdrawal_credentials=withdrawal_credentials,
110+
randao_commitment=randao_commitment,
111+
)
112+
proof_of_possession = sign_proof_of_possession(deposit_input, privkey_1, domain)
113+
# Add the first validator
114+
result_state, index = process_deposit(
115+
state=state,
116+
pubkey=pubkey_1,
117+
deposit=deposit,
118+
proof_of_possession=proof_of_possession,
119+
withdrawal_credentials=withdrawal_credentials,
120+
randao_commitment=randao_commitment,
121+
zero_balance_validator_ttl=zero_balance_validator_ttl,
122+
)
123+
124+
assert len(result_state.validator_registry) == 1
125+
index = 0
126+
assert result_state.validator_registry[0].pubkey == pubkey_1
127+
assert result_state.validator_registry[index].withdrawal_credentials == withdrawal_credentials
128+
assert result_state.validator_registry[index].randao_commitment == randao_commitment
129+
assert result_state.validator_registry[index].balance == deposit
130+
# test immutable
131+
assert len(state.validator_registry) == 0
132+
133+
# Add the second validator
134+
privkey_2 = privkeys[1]
135+
pubkey_2 = pubkeys[1]
136+
deposit_input = make_deposit_input(
137+
pubkey=pubkey_2,
138+
withdrawal_credentials=withdrawal_credentials,
139+
randao_commitment=randao_commitment,
140+
)
141+
proof_of_possession = sign_proof_of_possession(deposit_input, privkey_2, domain)
142+
result_state, index = process_deposit(
143+
state=result_state,
144+
pubkey=pubkey_2,
145+
deposit=deposit,
146+
proof_of_possession=proof_of_possession,
147+
withdrawal_credentials=withdrawal_credentials,
148+
randao_commitment=randao_commitment,
149+
zero_balance_validator_ttl=zero_balance_validator_ttl,
150+
)
151+
assert len(result_state.validator_registry) == 2
152+
assert result_state.validator_registry[1].pubkey == pubkey_2
153+
154+
# Force the first validator exited
155+
result_state = result_state.copy(
156+
validator_registry=(
157+
result_state.validator_registry[0].copy(
158+
balance=0,
159+
latest_status_change_slot=0,
160+
),
161+
result_state.validator_registry[1],
162+
)
163+
)
164+
165+
# Add the third validator
166+
privkey_3 = privkeys[2]
167+
pubkey_3 = pubkeys[2]
168+
deposit_input = make_deposit_input(
169+
pubkey=pubkey_3,
170+
withdrawal_credentials=withdrawal_credentials,
171+
randao_commitment=randao_commitment,
172+
)
173+
proof_of_possession = sign_proof_of_possession(deposit_input, privkey_3, domain)
174+
result_state, index = process_deposit(
175+
state=result_state,
176+
pubkey=pubkey_3,
177+
deposit=deposit,
178+
proof_of_possession=proof_of_possession,
179+
withdrawal_credentials=withdrawal_credentials,
180+
randao_commitment=randao_commitment,
181+
zero_balance_validator_ttl=zero_balance_validator_ttl,
182+
)
183+
assert len(result_state.validator_registry) == 2
184+
assert result_state.validator_registry[0].pubkey == pubkey_3

0 commit comments

Comments
 (0)