Skip to content

Commit a2f5785

Browse files
ralexstokeshwwhww
authored andcommitted
Add slashable helper (#1658)
* Adds the helper function `verify_slashable_vote_data` * Fix import * Various formatting fixes from the linter * Turn constants into function parameters * Use appropriate python instead of pseudocode from spec * Handle import/use of SignatureDomain enum properly * Some light refactoring while preferring tuples over lists Tuples are preferred in this codebase (over lists) due to their immutability. This commit also contains some other refactorings to break the verification down into small distinct steps (for better understandability). * Use 'forward references' for types that need to avoid import issues * Adjust grammar to be consistent with repo standard * formatting * Use more specific name for signature function * Formatting fixes to satisfy linter * Move messages generation to property of `SlashableVoteData` class * Insert missing class for "params" field into test fixture The prior implementation was passing a flat dictionary (rather than instantiating a class of the appropriate type with said dictionary) and the existing tests didn't have a problem with this as they were simply checking for equality of this dictionary. This yields a SlashableVoteData instance that fails rlp serialization however which is fixed by instantiating the appropriate class when we get the constructor arguments for this fixture * Add tests performing sanity checks on SlashableVoteData properties * Minimize the interface this function requests The function requests less now; this is good practice and if anything makes the tests easier to write * Add tests for `verify_slashable_vote_data` helper * Add `validators` property on BeaconState for convenience * Change hash of `SlashableVoteData` to be the attestation data This value is likely to change soon so we just want _some_ hash for now. We use the attestation data as before we were hashing the entire container including the signature putting us in a catch-22. * Update test to reflect updated hash * Remove unused import to satisfy linter * variety of linter fixes * formatting * Linter caught unused variable that is unnecessary Change allows for simplification of code * Linter caught unused function that, in fact, should be tested * Add additional test property * Code clean-up * Be more precise in how we "corrupt" the vote count * Remove property that may be confused for other representation * Formatting fixes, some small code adjustments based on feedback * Tighter test condition * Use a value for the errant `privkey` that produces invalid signature The previous value of `0` seems to produce a signature that verifies any message
1 parent 9c93531 commit a2f5785

File tree

5 files changed

+407
-7
lines changed

5 files changed

+407
-7
lines changed

eth/beacon/helpers.py

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,36 @@
2020
get_bitfield_length,
2121
has_voted,
2222
)
23+
import eth._utils.bls as bls
2324
from eth._utils.numeric import (
2425
clamp,
2526
)
2627

27-
from eth.beacon.block_committees_info import BlockCommitteesInfo
28-
from eth.beacon.types.shard_committees import ShardCommittee
29-
from eth.beacon.types.validator_registry_delta_block import ValidatorRegistryDeltaBlock
28+
from eth.beacon.types.validator_registry_delta_block import (
29+
ValidatorRegistryDeltaBlock,
30+
)
31+
from eth.beacon.block_committees_info import (
32+
BlockCommitteesInfo,
33+
)
34+
from eth.beacon.enums import (
35+
SignatureDomain,
36+
)
37+
from eth.beacon.types.shard_committees import (
38+
ShardCommittee,
39+
)
3040
from eth.beacon._utils.random import (
3141
shuffle,
3242
split,
3343
)
44+
import functools
3445

3546

3647
if TYPE_CHECKING:
37-
from eth.beacon.enums import SignatureDomain # noqa: F401
3848
from eth.beacon.types.attestation_data import AttestationData # noqa: F401
3949
from eth.beacon.types.blocks import BaseBeaconBlock # noqa: F401
4050
from eth.beacon.types.states import BeaconState # noqa: F401
4151
from eth.beacon.types.fork_data import ForkData # noqa: F401
52+
from eth.beacon.types.slashable_vote_data import SlashableVoteData # noqa: F401
4253
from eth.beacon.types.validator_records import ValidatorRecord # noqa: F401
4354

4455

@@ -387,7 +398,7 @@ def get_fork_version(fork_data: 'ForkData',
387398

388399
def get_domain(fork_data: 'ForkData',
389400
slot: int,
390-
domain_type: 'SignatureDomain') -> int:
401+
domain_type: SignatureDomain) -> int:
391402
"""
392403
Return the domain number of the current fork and ``domain_type``.
393404
"""
@@ -398,6 +409,72 @@ def get_domain(fork_data: 'ForkData',
398409
) * 4294967296 + domain_type
399410

400411

412+
@to_tuple
413+
def get_pubkey_for_indices(validators: Sequence['ValidatorRecord'],
414+
indices: Sequence[int]) -> Iterable[int]:
415+
for index in indices:
416+
yield validators[index].pubkey
417+
418+
419+
@to_tuple
420+
def generate_aggregate_pubkeys(validators: Sequence['ValidatorRecord'],
421+
vote_data: 'SlashableVoteData') -> Iterable[int]:
422+
"""
423+
Compute the aggregate pubkey we expect based on
424+
the proof-of-custody indices found in the ``vote_data``.
425+
"""
426+
proof_of_custody_0_indices = vote_data.aggregate_signature_poc_0_indices
427+
proof_of_custody_1_indices = vote_data.aggregate_signature_poc_1_indices
428+
all_indices = (proof_of_custody_0_indices, proof_of_custody_1_indices)
429+
get_pubkeys = functools.partial(get_pubkey_for_indices, validators)
430+
return map(
431+
bls.aggregate_pubkeys,
432+
map(get_pubkeys, all_indices),
433+
)
434+
435+
436+
def verify_vote_count(vote_data: 'SlashableVoteData', max_casper_votes: int) -> bool:
437+
"""
438+
Ensure we have no more than ``max_casper_votes`` in the ``vote_data``.
439+
"""
440+
return vote_data.vote_count <= max_casper_votes
441+
442+
443+
def verify_slashable_vote_data_signature(state: 'BeaconState',
444+
vote_data: 'SlashableVoteData') -> bool:
445+
"""
446+
Ensure we have a valid aggregate signature for the ``vote_data``.
447+
"""
448+
pubkeys = generate_aggregate_pubkeys(state.validator_registry, vote_data)
449+
450+
messages = vote_data.messages
451+
452+
signature = vote_data.aggregate_signature
453+
454+
domain = get_domain(state.fork_data, state.slot, SignatureDomain.DOMAIN_ATTESTATION)
455+
456+
return bls.verify_multiple(
457+
pubkeys=pubkeys,
458+
messages=messages,
459+
signature=signature,
460+
domain=domain,
461+
)
462+
463+
464+
def verify_slashable_vote_data(state: 'BeaconState',
465+
vote_data: 'SlashableVoteData',
466+
max_casper_votes: int) -> bool:
467+
"""
468+
Ensure that the ``vote_data`` is properly assembled and contains the signature
469+
we expect from the validators we expect. Otherwise, return False as
470+
the ``vote_data`` is invalid.
471+
"""
472+
return (
473+
verify_vote_count(vote_data, max_casper_votes) and
474+
verify_slashable_vote_data_signature(state, vote_data)
475+
)
476+
477+
401478
def is_double_vote(attestation_data_1: 'AttestationData',
402479
attestation_data_2: 'AttestationData') -> bool:
403480
"""

eth/beacon/types/slashable_vote_data.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
)
55
from typing import (
66
Sequence,
7+
Tuple,
78
)
9+
from eth_typing import (
10+
Hash32,
11+
)
12+
from eth.beacon._utils.hash import hash_eth2
813
from eth.rlp.sedes import (
914
uint24,
1015
uint384,
@@ -38,3 +43,39 @@ def __init__(self,
3843
data,
3944
aggregate_signature,
4045
)
46+
47+
_hash = None
48+
49+
@property
50+
def hash(self) -> Hash32:
51+
if self._hash is None:
52+
self._hash = hash_eth2(rlp.encode(self.data))
53+
return self._hash
54+
55+
@property
56+
def root(self) -> Hash32:
57+
# Alias of `hash`.
58+
# Using flat hash, will likely use SSZ tree hash.
59+
return self.hash
60+
61+
_vote_count = None
62+
63+
@property
64+
def vote_count(self) -> int:
65+
if self._vote_count is None:
66+
count_zero_indices = len(self.aggregate_signature_poc_0_indices)
67+
count_one_indices = len(self.aggregate_signature_poc_1_indices)
68+
self._vote_count = count_zero_indices + count_one_indices
69+
return self._vote_count
70+
71+
@property
72+
def messages(self) -> Tuple[bytes, bytes]:
73+
"""
74+
Build the messages that validators are expected to sign for a ``CasperSlashing`` operation.
75+
"""
76+
# TODO: change to hash_tree_root(vote_data) when we have SSZ tree hashing
77+
vote_data_root = self.root
78+
return (
79+
vote_data_root + (0).to_bytes(1, 'big'),
80+
vote_data_root + (1).to_bytes(1, 'big'),
81+
)

tests/beacon/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def sample_slashable_vote_data_params(sample_attestation_data_params):
283283
return {
284284
'aggregate_signature_poc_0_indices': (10, 11, 12, 15, 28),
285285
'aggregate_signature_poc_1_indices': (7, 8, 100, 131, 249),
286-
'data': sample_attestation_data_params,
286+
'data': AttestationData(**sample_attestation_data_params),
287287
'aggregate_signature': (1, 2, 3, 4, 5),
288288
}
289289

0 commit comments

Comments
 (0)