Skip to content

Commit 20441a4

Browse files
authored
Merge pull request #1612 from onyb/misc-new-helpers
Add new beacon helpers and implement tests
2 parents bfa1f2f + 2b0e4e6 commit 20441a4

File tree

4 files changed

+143
-5
lines changed

4 files changed

+143
-5
lines changed

eth/beacon/helpers.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@
2727
clamp,
2828
)
2929

30-
from eth.beacon.block_committees_info import BlockCommitteesInfo
31-
from eth.beacon.enums import (
32-
ValidatorStatusCode,
30+
from eth.beacon.block_committees_info import (
31+
BlockCommitteesInfo,
3332
)
3433
from eth.beacon.types.shard_committees import (
3534
ShardCommittee,
@@ -42,7 +41,7 @@
4241

4342
if TYPE_CHECKING:
4443
from eth.beacon.enums import SignatureDomain # noqa: F401
45-
from eth.beacon.types.attestation_records import AttestationRecord # noqa: F401
44+
from eth.beacon.types.attestation_data import AttestationData # noqa: F401
4645
from eth.beacon.types.blocks import BaseBeaconBlock # noqa: F401
4746
from eth.beacon.types.states import BeaconState # noqa: F401
4847
from eth.beacon.types.fork_data import ForkData # noqa: F401
@@ -139,7 +138,7 @@ def get_active_validator_indices(validators: Sequence['ValidatorRecord']) -> Tup
139138
"""
140139
return tuple(
141140
i for i, v in enumerate(validators)
142-
if v.status in [ValidatorStatusCode.ACTIVE, ValidatorStatusCode.ACTIVE_PENDING_EXIT]
141+
if v.is_active
143142
)
144143

145144

@@ -402,3 +401,32 @@ def get_domain(fork_data: 'ForkData',
402401
fork_data,
403402
slot,
404403
) * 4294967296 + domain_type
404+
405+
406+
def is_double_vote(attestation_data_1: 'AttestationData',
407+
attestation_data_2: 'AttestationData') -> bool:
408+
"""
409+
Assumes ``attestation_data_1`` is distinct from ``attestation_data_2``.
410+
411+
Returns True if the provided ``AttestationData`` are slashable
412+
due to a 'double vote'.
413+
"""
414+
return attestation_data_1.slot == attestation_data_2.slot
415+
416+
417+
def is_surround_vote(attestation_data_1: 'AttestationData',
418+
attestation_data_2: 'AttestationData') -> bool:
419+
"""
420+
Assumes ``attestation_data_1`` is distinct from ``attestation_data_2``.
421+
422+
Returns True if the provided ``AttestationData`` are slashable
423+
due to a 'surround vote'.
424+
425+
Note: parameter order matters as this function only checks
426+
that ``attestation_data_1`` surrounds ``attestation_data_2``.
427+
"""
428+
return (
429+
(attestation_data_1.justified_slot < attestation_data_2.justified_slot) and
430+
(attestation_data_2.justified_slot + 1 == attestation_data_2.slot) and
431+
(attestation_data_2.slot < attestation_data_1.slot)
432+
)

eth/beacon/types/validator_records.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@
33
)
44
import rlp
55

6+
from eth.beacon.enums import (
7+
ValidatorStatusCode,
8+
)
69
from eth.rlp.sedes import (
710
uint64,
811
uint256,
912
hash32,
1013
)
1114

1215

16+
VALIDATOR_RECORD_ACTIVE_STATUSES = {
17+
ValidatorStatusCode.ACTIVE,
18+
ValidatorStatusCode.ACTIVE_PENDING_EXIT,
19+
}
20+
21+
1322
class ValidatorRecord(rlp.Serializable):
1423
"""
1524
Note: using RLP until we have standardized serialization format.
@@ -52,3 +61,10 @@ def __init__(self,
5261
latest_status_change_slot=latest_status_change_slot,
5362
exit_count=exit_count,
5463
)
64+
65+
@property
66+
def is_active(self) -> bool:
67+
"""
68+
Returns ``True`` if the validator is active.
69+
"""
70+
return self.status in VALIDATOR_RECORD_ACTIVE_STATUSES

tests/beacon/test_helpers.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
from eth.beacon.enums import (
1414
ValidatorStatusCode,
1515
)
16+
from eth.beacon.types.attestation_data import (
17+
AttestationData,
18+
)
1619
from eth.beacon.types.blocks import BaseBeaconBlock
1720
from eth.beacon.types.fork_data import ForkData
1821
from eth.beacon.types.shard_committees import ShardCommittee
@@ -31,6 +34,8 @@
3134
get_new_validator_registry_delta_chain_tip,
3235
_get_shard_committees_at_slot,
3336
get_block_committees_info,
37+
is_double_vote,
38+
is_surround_vote,
3439
)
3540

3641

@@ -698,3 +703,66 @@ def test_get_domain(pre_fork_version,
698703
slot=current_slot,
699704
domain_type=domain_type,
700705
)
706+
707+
708+
def test_is_double_vote(sample_attestation_data_params):
709+
attestation_data_1_params = {
710+
**sample_attestation_data_params,
711+
'slot': 12345,
712+
}
713+
attestation_data_1 = AttestationData(**attestation_data_1_params)
714+
715+
attestation_data_2_params = {
716+
**sample_attestation_data_params,
717+
'slot': 12345,
718+
}
719+
attestation_data_2 = AttestationData(**attestation_data_2_params)
720+
721+
assert is_double_vote(attestation_data_1, attestation_data_2)
722+
723+
attestation_data_3_params = {
724+
**sample_attestation_data_params,
725+
'slot': 54321,
726+
}
727+
attestation_data_3 = AttestationData(**attestation_data_3_params)
728+
729+
assert not is_double_vote(attestation_data_1, attestation_data_3)
730+
731+
732+
@pytest.mark.parametrize(
733+
(
734+
'attestation_1_slot,'
735+
'attestation_1_justified_slot,'
736+
'attestation_2_slot,'
737+
'attestation_2_justified_slot,'
738+
'expected'
739+
),
740+
[
741+
(0, 0, 0, 0, False),
742+
(4, 3, 3, 2, False), # not (attestation_1_justified_slot < attestation_2_justified_slot
743+
(4, 0, 3, 1, False), # not (attestation_2_justified_slot + 1 == attestation_2_slot)
744+
(4, 0, 4, 3, False), # not (attestation_2_slot < attestation_1_slot)
745+
(4, 0, 3, 2, True),
746+
],
747+
)
748+
def test_is_surround_vote(sample_attestation_data_params,
749+
attestation_1_slot,
750+
attestation_1_justified_slot,
751+
attestation_2_slot,
752+
attestation_2_justified_slot,
753+
expected):
754+
attestation_data_1_params = {
755+
**sample_attestation_data_params,
756+
'slot': attestation_1_slot,
757+
'justified_slot': attestation_1_justified_slot,
758+
}
759+
attestation_data_1 = AttestationData(**attestation_data_1_params)
760+
761+
attestation_data_2_params = {
762+
**sample_attestation_data_params,
763+
'slot': attestation_2_slot,
764+
'justified_slot': attestation_2_justified_slot,
765+
}
766+
attestation_data_2 = AttestationData(**attestation_data_2_params)
767+
768+
assert is_surround_vote(attestation_data_1, attestation_data_2) == expected

tests/beacon/types/test_validator_record.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import pytest
2+
3+
from eth.beacon.enums import (
4+
ValidatorStatusCode,
5+
)
16
from eth.beacon.types.validator_records import (
27
ValidatorRecord,
38
)
@@ -7,3 +12,24 @@ def test_defaults(sample_validator_record_params):
712
validator = ValidatorRecord(**sample_validator_record_params)
813
assert validator.pubkey == sample_validator_record_params['pubkey']
914
assert validator.withdrawal_credentials == sample_validator_record_params['withdrawal_credentials'] # noqa: E501
15+
16+
17+
@pytest.mark.parametrize(
18+
'status,expected',
19+
[
20+
(ValidatorStatusCode.PENDING_ACTIVATION, False),
21+
(ValidatorStatusCode.ACTIVE, True),
22+
(ValidatorStatusCode.ACTIVE_PENDING_EXIT, True),
23+
(ValidatorStatusCode.EXITED_WITHOUT_PENALTY, False),
24+
(ValidatorStatusCode.EXITED_WITH_PENALTY, False),
25+
],
26+
)
27+
def test_is_active(sample_validator_record_params,
28+
status,
29+
expected):
30+
validator_record_params = {
31+
**sample_validator_record_params,
32+
'status': status
33+
}
34+
validator = ValidatorRecord(**validator_record_params)
35+
assert validator.is_active == expected

0 commit comments

Comments
 (0)