Merge branch 'dev' into vitalik81
This commit is contained in:
commit
2418bab250
|
@ -0,0 +1,41 @@
|
||||||
|
# Python CircleCI 2.0 configuration file
|
||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- image: circleci/python:3.6
|
||||||
|
working_directory: ~/repo
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
# Download and cache dependencies
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- v1-dependencies-{{ checksum "requirements.txt" }}
|
||||||
|
# fallback to using the latest cache if no exact match is found
|
||||||
|
- v1-dependencies-
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: install dependencies
|
||||||
|
command: |
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
- run:
|
||||||
|
name: build phase0 spec
|
||||||
|
command: make build/phase0
|
||||||
|
|
||||||
|
- save_cache:
|
||||||
|
paths:
|
||||||
|
- ./venv
|
||||||
|
key: v1-dependencies-{{ checksum "requirements.txt" }}
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: run tests
|
||||||
|
command: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pytest tests
|
||||||
|
|
||||||
|
- store_artifacts:
|
||||||
|
path: test-reports
|
||||||
|
destination: test-reports
|
|
@ -0,0 +1,7 @@
|
||||||
|
*.pyc
|
||||||
|
/__pycache__
|
||||||
|
/venv
|
||||||
|
/.pytest_cache
|
||||||
|
|
||||||
|
build/
|
||||||
|
output/
|
|
@ -0,0 +1,29 @@
|
||||||
|
SPEC_DIR = ./specs
|
||||||
|
SCRIPT_DIR = ./scripts
|
||||||
|
BUILD_DIR = ./build
|
||||||
|
UTILS_DIR = ./utils
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: clean all test
|
||||||
|
|
||||||
|
|
||||||
|
all: $(BUILD_DIR)/phase0
|
||||||
|
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
# runs a limited set of tests against a minimal config
|
||||||
|
# run pytest with `-m` option to full suite
|
||||||
|
test:
|
||||||
|
pytest -m minimal_config tests/
|
||||||
|
|
||||||
|
|
||||||
|
$(BUILD_DIR)/phase0:
|
||||||
|
mkdir -p $@
|
||||||
|
python3 $(SCRIPT_DIR)/phase0/build_spec.py $(SPEC_DIR)/core/0_beacon-chain.md $@/spec.py
|
||||||
|
mkdir -p $@/utils
|
||||||
|
cp $(UTILS_DIR)/phase0/* $@/utils
|
||||||
|
cp $(UTILS_DIR)/phase0/state_transition.py $@
|
||||||
|
touch $@/__init__.py $@/utils/__init__.py
|
|
@ -0,0 +1,6 @@
|
||||||
|
eth-utils>=1.3.0,<2
|
||||||
|
eth-typing>=2.1.0,<3.0.0
|
||||||
|
oyaml==0.7
|
||||||
|
pycryptodome==3.7.3
|
||||||
|
py_ecc>=1.6.0
|
||||||
|
pytest>=3.6,<3.7
|
|
@ -0,0 +1,79 @@
|
||||||
|
import sys
|
||||||
|
import function_puller
|
||||||
|
|
||||||
|
|
||||||
|
def build_spec(sourcefile, outfile):
|
||||||
|
code_lines = []
|
||||||
|
|
||||||
|
code_lines.append("from build.phase0.utils.minimal_ssz import *")
|
||||||
|
code_lines.append("from build.phase0.utils.bls_stub import *")
|
||||||
|
for i in (1, 2, 3, 4, 8, 32, 48, 96):
|
||||||
|
code_lines.append("def int_to_bytes%d(x): return x.to_bytes(%d, 'little')" % (i, i))
|
||||||
|
code_lines.append("SLOTS_PER_EPOCH = 64") # stub, will get overwritten by real var
|
||||||
|
code_lines.append("def slot_to_epoch(x): return x // SLOTS_PER_EPOCH")
|
||||||
|
|
||||||
|
code_lines.append("""
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
List,
|
||||||
|
NewType,
|
||||||
|
Tuple,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Slot = NewType('Slot', int) # uint64
|
||||||
|
Epoch = NewType('Epoch', int) # uint64
|
||||||
|
Shard = NewType('Shard', int) # uint64
|
||||||
|
ValidatorIndex = NewType('ValidatorIndex', int) # uint64
|
||||||
|
Gwei = NewType('Gwei', int) # uint64
|
||||||
|
Bytes32 = NewType('Bytes32', bytes) # bytes32
|
||||||
|
BLSPubkey = NewType('BLSPubkey', bytes) # bytes48
|
||||||
|
BLSSignature = NewType('BLSSignature', bytes) # bytes96
|
||||||
|
Any = None
|
||||||
|
Store = None
|
||||||
|
""")
|
||||||
|
|
||||||
|
code_lines += function_puller.get_lines(sourcefile)
|
||||||
|
|
||||||
|
code_lines.append("""
|
||||||
|
# Monkey patch validator get committee code
|
||||||
|
_compute_committee = compute_committee
|
||||||
|
committee_cache = {}
|
||||||
|
def compute_committee(validator_indices: List[ValidatorIndex],
|
||||||
|
seed: Bytes32,
|
||||||
|
index: int,
|
||||||
|
total_committees: int) -> List[ValidatorIndex]:
|
||||||
|
|
||||||
|
param_hash = (hash_tree_root(validator_indices), seed, index, total_committees)
|
||||||
|
|
||||||
|
if param_hash in committee_cache:
|
||||||
|
# print("Cache hit, epoch={0}".format(epoch))
|
||||||
|
return committee_cache[param_hash]
|
||||||
|
else:
|
||||||
|
# print("Cache miss, epoch={0}".format(epoch))
|
||||||
|
ret = _compute_committee(validator_indices, seed, index, total_committees)
|
||||||
|
committee_cache[param_hash] = ret
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# Monkey patch hash cache
|
||||||
|
_hash = hash
|
||||||
|
hash_cache = {}
|
||||||
|
def hash(x):
|
||||||
|
if x in hash_cache:
|
||||||
|
return hash_cache[x]
|
||||||
|
else:
|
||||||
|
ret = _hash(x)
|
||||||
|
hash_cache[x] = ret
|
||||||
|
return ret
|
||||||
|
""")
|
||||||
|
|
||||||
|
with open(outfile, 'w') as out:
|
||||||
|
out.write("\n".join(code_lines))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Error: spec source and outfile must defined")
|
||||||
|
build_spec(sys.argv[1], sys.argv[2])
|
|
@ -0,0 +1,46 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def get_lines(file_name):
|
||||||
|
code_lines = []
|
||||||
|
pulling_from = None
|
||||||
|
current_name = None
|
||||||
|
processing_typedef = False
|
||||||
|
for linenum, line in enumerate(open(sys.argv[1]).readlines()):
|
||||||
|
line = line.rstrip()
|
||||||
|
if pulling_from is None and len(line) > 0 and line[0] == '#' and line[-1] == '`':
|
||||||
|
current_name = line[line[:-1].rfind('`') + 1: -1]
|
||||||
|
if line[:9] == '```python':
|
||||||
|
assert pulling_from is None
|
||||||
|
pulling_from = linenum + 1
|
||||||
|
elif line[:3] == '```':
|
||||||
|
if pulling_from is None:
|
||||||
|
pulling_from = linenum
|
||||||
|
else:
|
||||||
|
if processing_typedef:
|
||||||
|
assert code_lines[-1] == '}'
|
||||||
|
code_lines[-1] = '})'
|
||||||
|
pulling_from = None
|
||||||
|
processing_typedef = False
|
||||||
|
else:
|
||||||
|
if pulling_from == linenum and line == '{':
|
||||||
|
code_lines.append('%s = SSZType({' % current_name)
|
||||||
|
processing_typedef = True
|
||||||
|
elif pulling_from is not None:
|
||||||
|
code_lines.append(line)
|
||||||
|
elif pulling_from is None and len(line) > 0 and line[0] == '|':
|
||||||
|
row = line[1:].split('|')
|
||||||
|
if len(row) >= 2:
|
||||||
|
for i in range(2):
|
||||||
|
row[i] = row[i].strip().strip('`')
|
||||||
|
if '`' in row[i]:
|
||||||
|
row[i] = row[i][:row[i].find('`')]
|
||||||
|
eligible = True
|
||||||
|
if row[0][0] not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_':
|
||||||
|
eligible = False
|
||||||
|
for c in row[0]:
|
||||||
|
if c not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789':
|
||||||
|
eligible = False
|
||||||
|
if eligible:
|
||||||
|
code_lines.append(row[0] + ' = ' + (row[1].replace('**TBD**', '0x1234567890123567890123456789012357890')))
|
||||||
|
return code_lines
|
|
@ -110,11 +110,11 @@ def modular_squareroot(value: Fq2) -> Fq2:
|
||||||
|
|
||||||
### `bls_aggregate_pubkeys`
|
### `bls_aggregate_pubkeys`
|
||||||
|
|
||||||
Let `bls_aggregate_pubkeys(pubkeys: List[Bytes48]) -> Bytes48` return `pubkeys[0] + .... + pubkeys[len(pubkeys)-1]`, where `+` is the elliptic curve addition operation over the G1 curve.
|
Let `bls_aggregate_pubkeys(pubkeys: List[Bytes48]) -> Bytes48` return `pubkeys[0] + .... + pubkeys[len(pubkeys)-1]`, where `+` is the elliptic curve addition operation over the G1 curve. (When `len(pubkeys) == 0` the empty sum is the G1 point at infinity.)
|
||||||
|
|
||||||
### `bls_aggregate_signatures`
|
### `bls_aggregate_signatures`
|
||||||
|
|
||||||
Let `bls_aggregate_signatures(signatures: List[Bytes96]) -> Bytes96` return `signatures[0] + .... + signatures[len(signatures)-1]`, where `+` is the elliptic curve addition operation over the G2 curve.
|
Let `bls_aggregate_signatures(signatures: List[Bytes96]) -> Bytes96` return `signatures[0] + .... + signatures[len(signatures)-1]`, where `+` is the elliptic curve addition operation over the G2 curve. (When `len(signatures) == 0` the empty sum is the G2 point at infinity.)
|
||||||
|
|
||||||
## Signature verification
|
## Signature verification
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,499 @@
|
||||||
|
# Ethereum 2.0 Phase 1 -- Custody Game
|
||||||
|
|
||||||
|
**NOTICE**: This spec is a work-in-progress for researchers and implementers.
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [Ethereum 2.0 Phase 1 -- Custody Game](#ethereum-20-phase-1----custody-game)
|
||||||
|
- [Table of contents](#table-of-contents)
|
||||||
|
- [Introduction](#introduction)
|
||||||
|
- [Terminology](#terminology)
|
||||||
|
- [Constants](#constants)
|
||||||
|
- [Misc](#misc)
|
||||||
|
- [Time parameters](#time-parameters)
|
||||||
|
- [Max transactions per block](#max-transactions-per-block)
|
||||||
|
- [Signature domains](#signature-domains)
|
||||||
|
- [Data structures](#data-structures)
|
||||||
|
- [Custody objects](#custody-objects)
|
||||||
|
- [`CustodyChunkChallenge`](#custodychunkchallenge)
|
||||||
|
- [`CustodyBitChallenge`](#custodybitchallenge)
|
||||||
|
- [`CustodyChunkChallengeRecord`](#custodychunkchallengerecord)
|
||||||
|
- [`CustodyBitChallengeRecord`](#custodybitchallengerecord)
|
||||||
|
- [`CustodyResponse`](#custodyresponse)
|
||||||
|
- [`CustodyKeyReveal`](#custodykeyreveal)
|
||||||
|
- [Phase 0 container updates](#phase-0-container-updates)
|
||||||
|
- [`Validator`](#validator)
|
||||||
|
- [`BeaconState`](#beaconstate)
|
||||||
|
- [`BeaconBlockBody`](#beaconblockbody)
|
||||||
|
- [Helpers](#helpers)
|
||||||
|
- [`get_crosslink_chunk_count`](#get_crosslink_chunk_count)
|
||||||
|
- [`get_custody_chunk_bit`](#get_custody_chunk_bit)
|
||||||
|
- [`epoch_to_custody_period`](#epoch_to_custody_period)
|
||||||
|
- [`verify_custody_key`](#verify_custody_key)
|
||||||
|
- [Per-block processing](#per-block-processing)
|
||||||
|
- [Transactions](#transactions)
|
||||||
|
- [Custody reveals](#custody-reveals)
|
||||||
|
- [Chunk challenges](#chunk-challenges)
|
||||||
|
- [Bit challenges](#bit-challenges)
|
||||||
|
- [Custody responses](#custody-responses)
|
||||||
|
- [Per-epoch processing](#per-epoch-processing)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This document details the beacon chain additions and changes in Phase 1 of Ethereum 2.0 to support the shard data custody game, building upon the [phase 0](0_beacon-chain.md) specification.
|
||||||
|
|
||||||
|
## Terminology
|
||||||
|
|
||||||
|
* **Custody game**:
|
||||||
|
* **Custody period**:
|
||||||
|
* **Custody chunk**:
|
||||||
|
* **Custody chunk bit**:
|
||||||
|
* **Custody chunk challenge**:
|
||||||
|
* **Custody bit**:
|
||||||
|
* **Custody bit challenge**:
|
||||||
|
* **Custody key**:
|
||||||
|
* **Custody key reveal**:
|
||||||
|
* **Custody key mask**:
|
||||||
|
* **Custody response**:
|
||||||
|
* **Custody response deadline**:
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
| Name | Value |
|
||||||
|
| - | - |
|
||||||
|
| `BYTES_PER_SHARD_BLOCK` | `2**14` (= 16,384) |
|
||||||
|
| `BYTES_PER_CUSTODY_CHUNK` | `2**9` (= 512) |
|
||||||
|
| `MINOR_REWARD_QUOTIENT` | `2**8` (= 256) |
|
||||||
|
|
||||||
|
### Time parameters
|
||||||
|
|
||||||
|
| Name | Value | Unit | Duration |
|
||||||
|
| - | - | :-: | :-: |
|
||||||
|
| `MAX_CHUNK_CHALLENGE_DELAY` | `2**11` (= 2,048) | epochs | ~9 days |
|
||||||
|
| `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days |
|
||||||
|
| `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days |
|
||||||
|
|
||||||
|
### Max transactions per block
|
||||||
|
|
||||||
|
| Name | Value |
|
||||||
|
| - | - |
|
||||||
|
| `MAX_CUSTODY_KEY_REVEALS` | `2**4` (= 16) |
|
||||||
|
| `MAX_CUSTODY_CHUNK_CHALLENGES` | `2**2` (= 4) |
|
||||||
|
| `MAX_CUSTODY_BIT_CHALLENGES` | `2**2` (= 4) |
|
||||||
|
| `MAX_CUSTODY_RESPONSES` | `2**5` (= 32) |
|
||||||
|
|
||||||
|
### Signature domains
|
||||||
|
|
||||||
|
| Name | Value |
|
||||||
|
| - | - |
|
||||||
|
| `DOMAIN_CUSTODY_KEY_REVEAL` | `6` |
|
||||||
|
| `DOMAIN_CUSTODY_BIT_CHALLENGE` | `7` |
|
||||||
|
|
||||||
|
## Data structures
|
||||||
|
|
||||||
|
### Custody objects
|
||||||
|
|
||||||
|
#### `CustodyChunkChallenge`
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'responder_index': ValidatorIndex,
|
||||||
|
'attestation': Attestation,
|
||||||
|
'chunk_index': 'uint64',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `CustodyBitChallenge`
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'responder_index': ValidatorIndex,
|
||||||
|
'attestation': Attestation,
|
||||||
|
'challenger_index': ValidatorIndex,
|
||||||
|
'responder_key': BLSSignature,
|
||||||
|
'chunk_bits': Bitfield,
|
||||||
|
'signature': BLSSignature,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `CustodyChunkChallengeRecord`
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'challenge_index': 'uint64',
|
||||||
|
'challenger_index': ValidatorIndex,
|
||||||
|
'responder_index': ValidatorIndex,
|
||||||
|
'deadline': Epoch,
|
||||||
|
'crosslink_data_root': Hash,
|
||||||
|
'depth': 'uint64',
|
||||||
|
'chunk_index': 'uint64',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `CustodyBitChallengeRecord`
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'challenge_index': 'uint64',
|
||||||
|
'challenger_index': ValidatorIndex,
|
||||||
|
'responder_index': ValidatorIndex,
|
||||||
|
'deadline': Epoch,
|
||||||
|
'crosslink_data_root': Hash,
|
||||||
|
'chunk_bits': Bitfield,
|
||||||
|
'responder_key': BLSSignature,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `CustodyResponse`
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'challenge_index': 'uint64',
|
||||||
|
'chunk_index': 'uint64',
|
||||||
|
'chunk': ['byte', BYTES_PER_CUSTODY_CHUNK],
|
||||||
|
'branch': [Hash],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `CustodyKeyReveal`
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'revealer_index': ValidatorIndex,
|
||||||
|
'period': 'uint64',
|
||||||
|
'key': BLSSignature,
|
||||||
|
'masker_index': ValidatorIndex,
|
||||||
|
'mask': Hash,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 0 container updates
|
||||||
|
|
||||||
|
Add the following fields to the end of the specified container objects. Fields with underlying type `uint64` are initialized to `0` and list fields are initialized to `[]`.
|
||||||
|
|
||||||
|
#### `Validator`
|
||||||
|
|
||||||
|
```python
|
||||||
|
'custody_reveal_index': 'uint64',
|
||||||
|
'max_reveal_lateness': 'uint64',
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `BeaconState`
|
||||||
|
|
||||||
|
```python
|
||||||
|
'custody_chunk_challenge_records': [CustodyChunkChallengeRecord],
|
||||||
|
'custody_bit_challenge_records': [CustodyBitChallengeRecord],
|
||||||
|
'custody_challenge_index': 'uint64',
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `BeaconBlockBody`
|
||||||
|
|
||||||
|
```python
|
||||||
|
'custody_key_reveals': [CustodyKeyReveal],
|
||||||
|
'custody_chunk_challenges': [CustodyChunkChallenge],
|
||||||
|
'custody_bit_challenges': [CustodyBitChallenge],
|
||||||
|
'custody_responses': [CustodyResponse],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helpers
|
||||||
|
|
||||||
|
### `get_crosslink_chunk_count`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_custody_chunk_count(attestation: Attestation) -> int:
|
||||||
|
crosslink_start_epoch = attestation.data.latest_crosslink.epoch
|
||||||
|
crosslink_end_epoch = slot_to_epoch(attestation.data.slot)
|
||||||
|
crosslink_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch)
|
||||||
|
chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * SLOTS_PER_EPOCH // BYTES_PER_CUSTODY_CHUNK
|
||||||
|
return crosslink_crosslink_length * chunks_per_epoch
|
||||||
|
```
|
||||||
|
|
||||||
|
### `get_custody_chunk_bit`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_custody_chunk_bit(key: BLSSignature, chunk: bytes) -> bool:
|
||||||
|
# TODO: Replace with something MPC-friendly, e.g. the Legendre symbol
|
||||||
|
return get_bitfield_bit(hash(challenge.responder_key + chunk), 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `epoch_to_custody_period`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def epoch_to_custody_period(epoch: Epoch) -> int:
|
||||||
|
return epoch // EPOCHS_PER_CUSTODY_PERIOD
|
||||||
|
```
|
||||||
|
|
||||||
|
### `verify_custody_key`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def verify_custody_key(state: BeaconState, reveal: CustodyKeyReveal) -> bool:
|
||||||
|
# Case 1: non-masked non-punitive non-early reveal
|
||||||
|
pubkeys = [state.validator_registry[reveal.revealer_index].pubkey]
|
||||||
|
message_hashes = [hash_tree_root(reveal.period)]
|
||||||
|
|
||||||
|
# Case 2: masked punitive early reveal
|
||||||
|
# Masking prevents proposer stealing the whistleblower reward
|
||||||
|
# Secure under the aggregate extraction infeasibility assumption
|
||||||
|
# See pages 11-12 of https://crypto.stanford.edu/~dabo/pubs/papers/aggreg.pdf
|
||||||
|
if reveal.mask != ZERO_HASH:
|
||||||
|
pubkeys.append(state.validator_registry[reveal.masker_index].pubkey)
|
||||||
|
message_hashes.append(reveal.mask)
|
||||||
|
|
||||||
|
return bls_verify_multiple(
|
||||||
|
pubkeys=pubkeys,
|
||||||
|
message_hashes=message_hashes,
|
||||||
|
signature=reveal.key,
|
||||||
|
domain=get_domain(
|
||||||
|
fork=state.fork,
|
||||||
|
epoch=reveal.period * EPOCHS_PER_CUSTODY_PERIOD,
|
||||||
|
domain_type=DOMAIN_CUSTODY_KEY_REVEAL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Per-block processing
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
|
||||||
|
Add the following transactions to the per-block processing, in order the given below and after all other transactions in phase 0.
|
||||||
|
|
||||||
|
#### Custody reveals
|
||||||
|
|
||||||
|
Verify that `len(block.body.custody_key_reveals) <= MAX_CUSTODY_KEY_REVEALS`.
|
||||||
|
|
||||||
|
For each `reveal` in `block.body.custody_key_reveals`, run the following function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_custody_reveal(state: BeaconState,
|
||||||
|
reveal: CustodyKeyReveal) -> None:
|
||||||
|
assert verify_custody_key(state, reveal)
|
||||||
|
revealer = state.validator_registry[reveal.revealer_index]
|
||||||
|
current_custody_period = epoch_to_custody_period(get_current_epoch(state))
|
||||||
|
|
||||||
|
# Case 1: non-masked non-punitive non-early reveal
|
||||||
|
if reveal.mask == ZERO_HASH:
|
||||||
|
assert reveal.period == epoch_to_custody_period(revealer.activation_epoch) + revealer.custody_reveal_index
|
||||||
|
# Revealer is active or exited
|
||||||
|
assert is_active_validator(revealer, get_current_epoch(state)) or revealer.exit_epoch > get_current_epoch(state)
|
||||||
|
revealer.custody_reveal_index += 1
|
||||||
|
revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, current_custody_period - reveal.period)
|
||||||
|
proposer_index = get_beacon_proposer_index(state, state.slot)
|
||||||
|
increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT)
|
||||||
|
|
||||||
|
# Case 2: masked punitive early reveal
|
||||||
|
else:
|
||||||
|
assert reveal.period > current_custody_period
|
||||||
|
assert revealer.slashed is False
|
||||||
|
slash_validator(state, reveal.revealer_index, reveal.masker_index)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Chunk challenges
|
||||||
|
|
||||||
|
Verify that `len(block.body.custody_chunk_challenges) <= MAX_CUSTODY_CHUNK_CHALLENGES`.
|
||||||
|
|
||||||
|
For each `challenge` in `block.body.custody_chunk_challenges`, run the following function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_chunk_challenge(state: BeaconState,
|
||||||
|
challenge: CustodyChunkChallenge) -> None:
|
||||||
|
# Verify the attestation
|
||||||
|
assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation))
|
||||||
|
# Verify it is not too late to challenge
|
||||||
|
assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY
|
||||||
|
responder = state.validator_registry[challenge.responder_index]
|
||||||
|
assert responder.exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY
|
||||||
|
# Verify the responder participated in the attestation
|
||||||
|
attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield)
|
||||||
|
assert challenge.responder_index in attesters
|
||||||
|
# Verify the challenge is not a duplicate
|
||||||
|
for record in state.custody_chunk_challenge_records:
|
||||||
|
assert (
|
||||||
|
record.crosslink_data_root != challenge.attestation.data.crosslink_data_root or
|
||||||
|
record.chunk_index != challenge.chunk_index
|
||||||
|
)
|
||||||
|
# Verify depth
|
||||||
|
depth = math.log2(next_power_of_two(get_custody_chunk_count(challenge.attestation)))
|
||||||
|
assert challenge.chunk_index < 2**depth
|
||||||
|
# Add new chunk challenge record
|
||||||
|
state.custody_chunk_challenge_records.append(CustodyChunkChallengeRecord(
|
||||||
|
challenge_index=state.custody_challenge_index,
|
||||||
|
challenger_index=get_beacon_proposer_index(state, state.slot),
|
||||||
|
responder_index=challenge.responder_index
|
||||||
|
deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE,
|
||||||
|
crosslink_data_root=challenge.attestation.data.crosslink_data_root,
|
||||||
|
depth=depth,
|
||||||
|
chunk_index=challenge.chunk_index,
|
||||||
|
))
|
||||||
|
state.custody_challenge_index += 1
|
||||||
|
# Postpone responder withdrawability
|
||||||
|
responder.withdrawable_epoch = FAR_FUTURE_EPOCH
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Bit challenges
|
||||||
|
|
||||||
|
Verify that `len(block.body.custody_bit_challenges) <= MAX_CUSTODY_BIT_CHALLENGES`.
|
||||||
|
|
||||||
|
For each `challenge` in `block.body.custody_bit_challenges`, run the following function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_bit_challenge(state: BeaconState,
|
||||||
|
challenge: CustodyBitChallenge) -> None:
|
||||||
|
# Verify challenge signature
|
||||||
|
challenger = state.validator_registry[challenge.challenger_index]
|
||||||
|
assert bls_verify(
|
||||||
|
pubkey=challenger.pubkey,
|
||||||
|
message_hash=signed_root(challenge),
|
||||||
|
signature=challenge.signature,
|
||||||
|
domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_BIT_CHALLENGE),
|
||||||
|
)
|
||||||
|
# Verify the challenger is not slashed
|
||||||
|
assert challenger.slashed is False
|
||||||
|
# Verify the attestation
|
||||||
|
assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation))
|
||||||
|
# Verify the attestation is eligible for challenging
|
||||||
|
responder = state.validator_registry[challenge.responder_index]
|
||||||
|
min_challengeable_epoch = responder.exit_epoch - EPOCHS_PER_CUSTODY_PERIOD * (1 + responder.max_reveal_lateness)
|
||||||
|
assert min_challengeable_epoch <= slot_to_epoch(challenge.attestation.data.slot)
|
||||||
|
# Verify the responder participated in the attestation
|
||||||
|
attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield)
|
||||||
|
assert challenge.responder_index in attesters
|
||||||
|
# A validator can be the challenger or responder for at most one challenge at a time
|
||||||
|
for record in state.custody_bit_challenge_records:
|
||||||
|
assert record.challenger_index != challenge.challenger_index
|
||||||
|
assert record.responder_index != challenge.responder_index
|
||||||
|
# Verify the responder key
|
||||||
|
assert verify_custody_key(state, CustodyKeyReveal(
|
||||||
|
revealer_index=challenge.responder_index,
|
||||||
|
period=epoch_to_custody_period(slot_to_epoch(attestation.data.slot)),
|
||||||
|
key=challenge.responder_key,
|
||||||
|
masker_index=0,
|
||||||
|
mask=ZERO_HASH,
|
||||||
|
))
|
||||||
|
# Verify the chunk count
|
||||||
|
chunk_count = get_custody_chunk_count(challenge.attestation)
|
||||||
|
assert verify_bitfield(challenge.chunk_bits, chunk_count)
|
||||||
|
# Verify the xor of the chunk bits does not equal the custody bit
|
||||||
|
chunk_bits_xor = 0b0
|
||||||
|
for i in range(chunk_count):
|
||||||
|
chunk_bits_xor ^ get_bitfield_bit(challenge.chunk_bits, i)
|
||||||
|
custody_bit = get_bitfield_bit(attestation.custody_bitfield, attesters.index(responder_index))
|
||||||
|
assert custody_bit != chunk_bits_xor
|
||||||
|
# Add new bit challenge record
|
||||||
|
state.custody_bit_challenge_records.append(CustodyBitChallengeRecord(
|
||||||
|
challenge_index=state.custody_challenge_index,
|
||||||
|
challenger_index=challenge.challenger_index,
|
||||||
|
responder_index=challenge.responder_index,
|
||||||
|
deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE
|
||||||
|
crosslink_data_root=challenge.attestation.crosslink_data_root,
|
||||||
|
chunk_bits=challenge.chunk_bits,
|
||||||
|
responder_key=challenge.responder_key,
|
||||||
|
))
|
||||||
|
state.custody_challenge_index += 1
|
||||||
|
# Postpone responder withdrawability
|
||||||
|
responder.withdrawable_epoch = FAR_FUTURE_EPOCH
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custody responses
|
||||||
|
|
||||||
|
Verify that `len(block.body.custody_responses) <= MAX_CUSTODY_RESPONSES`.
|
||||||
|
|
||||||
|
For each `response` in `block.body.custody_responses`, run the following function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_custody_response(state: BeaconState,
|
||||||
|
response: CustodyResponse) -> None:
|
||||||
|
chunk_challenge = next(record for record in state.custody_chunk_challenge_records if record.challenge_index == response.challenge_index, None)
|
||||||
|
if chunk_challenge is not None:
|
||||||
|
return process_chunk_challenge_response(state, response, chunk_challenge)
|
||||||
|
|
||||||
|
bit_challenge = next(record for record in state.custody_bit_challenge_records if record.challenge_index == response.challenge_index, None)
|
||||||
|
if bit_challenge is not None:
|
||||||
|
return process_bit_challenge_response(state, response, bit_challenge)
|
||||||
|
|
||||||
|
assert False
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_chunk_challenge_response(state: BeaconState,
|
||||||
|
response: CustodyResponse,
|
||||||
|
challenge: CustodyChunkChallengeRecord) -> None:
|
||||||
|
# Verify chunk index
|
||||||
|
assert response.chunk_index == challenge.chunk_index
|
||||||
|
# Verify the chunk matches the crosslink data root
|
||||||
|
assert verify_merkle_branch(
|
||||||
|
leaf=hash_tree_root(response.chunk),
|
||||||
|
branch=response.branch,
|
||||||
|
depth=challenge.depth,
|
||||||
|
index=response.chunk_index,
|
||||||
|
root=challenge.crosslink_data_root,
|
||||||
|
)
|
||||||
|
# Clear the challenge
|
||||||
|
state.custody_chunk_challenge_records.remove(challenge)
|
||||||
|
# Reward the proposer
|
||||||
|
proposer_index = get_beacon_proposer_index(state, state.slot)
|
||||||
|
increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT)
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_bit_challenge_response(state: BeaconState,
|
||||||
|
response: CustodyResponse,
|
||||||
|
challenge: CustodyBitChallengeRecord) -> None:
|
||||||
|
# Verify chunk index
|
||||||
|
assert response.chunk_index < len(challenge.chunk_bits)
|
||||||
|
# Verify the chunk matches the crosslink data root
|
||||||
|
assert verify_merkle_branch(
|
||||||
|
leaf=hash_tree_root(response.chunk),
|
||||||
|
branch=response.branch,
|
||||||
|
depth=math.log2(next_power_of_two(len(challenge.chunk_bits))),
|
||||||
|
index=response.chunk_index,
|
||||||
|
root=challenge.crosslink_data_root,
|
||||||
|
)
|
||||||
|
# Verify the chunk bit does not match the challenge chunk bit
|
||||||
|
assert get_custody_chunk_bit(challenge.responder_key, response.chunk) != get_bitfield_bit(challenge.chunk_bits, response.chunk_index)
|
||||||
|
# Clear the challenge
|
||||||
|
state.custody_bit_challenge_records.remove(challenge)
|
||||||
|
# Slash challenger
|
||||||
|
slash_validator(state, challenge.challenger_index, challenge.responder_index)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Per-epoch processing
|
||||||
|
|
||||||
|
Run `process_challenge_deadlines(state)` immediately after `process_ejections(state)`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def process_challenge_deadlines(state: BeaconState) -> None:
|
||||||
|
for challenge in state.custody_chunk_challenge_records:
|
||||||
|
if get_current_epoch(state) > challenge.deadline:
|
||||||
|
slash_validator(state, challenge.responder_index, challenge.challenger_index)
|
||||||
|
state.custody_chunk_challenge_records.remove(challenge)
|
||||||
|
|
||||||
|
for challenge in state.custody_bit_challenge_records:
|
||||||
|
if get_current_epoch(state) > challenge.deadline:
|
||||||
|
slash_validator(state, challenge.responder_index, challenge.challenger_index)
|
||||||
|
state.custody_bit_challenge_records.remove(challenge)
|
||||||
|
```
|
||||||
|
|
||||||
|
In `process_penalties_and_exits`, change the definition of `eligible` to the following (note that it is not a pure function because `state` is declared in the surrounding scope):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def eligible(index):
|
||||||
|
validator = state.validator_registry[index]
|
||||||
|
# Cannot exit if there are still open chunk challenges
|
||||||
|
if len([record for record in state.custody_chunk_challenge_records if record.responder_index == index]) > 0:
|
||||||
|
return False
|
||||||
|
# Cannot exit if you have not revealed all of your custody keys
|
||||||
|
elif epoch_to_custody_period(revealer.activation_epoch) + validator.custody_reveal_index <= epoch_to_custody_period(validator.exit_epoch):
|
||||||
|
return False
|
||||||
|
# Cannot exit if you already have
|
||||||
|
elif validator.withdrawable_epoch < FAR_FUTURE_EPOCH:
|
||||||
|
return False
|
||||||
|
# Return minimum time
|
||||||
|
else:
|
||||||
|
return current_epoch >= validator.exit_epoch + MIN_VALIDATOR_WITHDRAWAL_EPOCHS
|
||||||
|
```
|
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +1,18 @@
|
||||||
**NOTICE**: This document is a work-in-progress for researchers and implementers.
|
# Beacon Chain Light Client Syncing
|
||||||
|
|
||||||
# Beacon chain light client syncing
|
__NOTICE__: This document is a work-in-progress for researchers and implementers. One of the design goals of the eth2 beacon chain is light-client friendlines, both to allow low-resource clients (mobile phones, IoT, etc) to maintain access to the blockchain in a reasonably safe way, but also to facilitate the development of "bridges" between the eth2 beacon chain and other chains.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
- [Beacon Chain Light Client Syncing](#beacon-chain-light-client-syncing)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Light client state](#light-client-state)
|
||||||
|
- [Updating the shuffled committee](#updating-the-shuffled-committee)
|
||||||
|
- [Computing the current committee](#computing-the-current-committee)
|
||||||
|
- [Verifying blocks](#verifying-blocks)
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
One of the design goals of the eth2 beacon chain is light-client friendlines, both to allow low-resource clients (mobile phones, IoT, etc) to maintain access to the blockchain in a reasonably safe way, but also to facilitate the development of "bridges" between the eth2 beacon chain and other chains.
|
|
||||||
|
|
||||||
### Preliminaries
|
### Preliminaries
|
||||||
|
|
||||||
|
@ -112,6 +122,7 @@ def compute_committee(header: BeaconBlockHeader,
|
||||||
bytes_to_int(hash(validator_memory.earlier_period_data.seed + bytes3(index))[0:8]) %
|
bytes_to_int(hash(validator_memory.earlier_period_data.seed + bytes3(index))[0:8]) %
|
||||||
PERSISTENT_COMMITTEE_PERIOD
|
PERSISTENT_COMMITTEE_PERIOD
|
||||||
)
|
)
|
||||||
|
|
||||||
# Take not-yet-cycled-out validators from earlier committee and already-cycled-in validators from
|
# Take not-yet-cycled-out validators from earlier committee and already-cycled-in validators from
|
||||||
# later committee; return a sorted list of the union of the two, deduplicated
|
# later committee; return a sorted list of the union of the two, deduplicated
|
||||||
return sorted(list(set(
|
return sorted(list(set(
|
||||||
|
@ -158,4 +169,5 @@ def verify_block_validity_proof(proof: BlockValidityProof, validator_memory: Val
|
||||||
domain=get_domain(state, slot_to_epoch(shard_block.slot), DOMAIN_SHARD_ATTESTER)
|
domain=get_domain(state, slot_to_epoch(shard_block.slot), DOMAIN_SHARD_ATTESTER)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
The size of this proof is only 200 (header) + 96 (signature) + 16 (bitfield) + 352 (shard block) = 664 bytes. It can be reduced further by replacing `ShardBlock` with `MerklePartial(lambda x: x.beacon_chain_ref, ShardBlock)`, which would cut off ~220 bytes.
|
The size of this proof is only 200 (header) + 96 (signature) + 16 (bitfield) + 352 (shard block) = 664 bytes. It can be reduced further by replacing `ShardBlock` with `MerklePartial(lambda x: x.beacon_chain_ref, ShardBlock)`, which would cut off ~220 bytes.
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
ETH 2.0 Networking Spec - Messaging
|
||||||
|
===
|
||||||
|
|
||||||
|
# Abstract
|
||||||
|
|
||||||
|
This specification describes how individual Ethereum 2.0 messages are represented on the wire.
|
||||||
|
|
||||||
|
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL”, NOT", “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
|
||||||
|
|
||||||
|
# Motivation
|
||||||
|
|
||||||
|
This specification seeks to define a messaging protocol that is flexible enough to be changed easily as the ETH 2.0 specification evolves.
|
||||||
|
|
||||||
|
Note that while `libp2p` is the chosen networking stack for Ethereum 2.0, as of this writing some clients do not have workable `libp2p` implementations. To allow those clients to communicate, we define a message envelope that includes the body's compression, encoding, and body length. Once `libp2p` is available across all implementations, this message envelope will be removed because `libp2p` will negotiate the values defined in the envelope upfront.
|
||||||
|
|
||||||
|
# Specification
|
||||||
|
|
||||||
|
## Message Structure
|
||||||
|
|
||||||
|
An ETH 2.0 message consists of an envelope that defines the message's compression, encoding, and length followed by the body itself.
|
||||||
|
|
||||||
|
Visually, a message looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
+--------------------------+
|
||||||
|
| compression nibble |
|
||||||
|
+--------------------------+
|
||||||
|
| encoding nibble |
|
||||||
|
+--------------------------+
|
||||||
|
| body length (uint64) |
|
||||||
|
+--------------------------+
|
||||||
|
| |
|
||||||
|
| body |
|
||||||
|
| |
|
||||||
|
+--------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients MUST ignore messages with mal-formed bodies. The compression/encoding nibbles MUST be one of the following values:
|
||||||
|
|
||||||
|
## Compression Nibble Values
|
||||||
|
|
||||||
|
- `0x0`: no compression
|
||||||
|
|
||||||
|
## Encoding Nibble Values
|
||||||
|
|
||||||
|
- `0x1`: SSZ
|
|
@ -0,0 +1,32 @@
|
||||||
|
ETH 2.0 Networking Spec - Node Identification
|
||||||
|
===
|
||||||
|
|
||||||
|
# Abstract
|
||||||
|
|
||||||
|
This specification describes how Ethereum 2.0 nodes identify and address each other on the network.
|
||||||
|
|
||||||
|
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL", NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||||
|
|
||||||
|
# Specification
|
||||||
|
|
||||||
|
Clients use Ethereum Node Records (as described in [EIP-778](http://eips.ethereum.org/EIPS/eip-778)) to discover one another. Each ENR includes, among other things, the following keys:
|
||||||
|
|
||||||
|
- The node's IP.
|
||||||
|
- The node's TCP port.
|
||||||
|
- The node's public key.
|
||||||
|
|
||||||
|
For clients to be addressable, their ENR responses MUST contain all of the above keys. Client MUST verify the signature of any received ENRs, and disconnect from peers whose ENR signatures are invalid. Each node's public key MUST be unique.
|
||||||
|
|
||||||
|
The keys above are enough to construct a [multiaddr](https://github.com/multiformats/multiaddr) for use with the rest of the `libp2p` stack.
|
||||||
|
|
||||||
|
It is RECOMMENDED that clients set their TCP port to the default of `9000`.
|
||||||
|
|
||||||
|
## Peer ID Generation
|
||||||
|
|
||||||
|
The `libp2p` networking stack identifies peers via a "peer ID." Simply put, a node's Peer ID is the SHA2-256 `multihash` of the node's public key struct (serialized in protobuf, refer to the [Peer ID spec](https://github.com/libp2p/specs/pull/100)). `go-libp2p-crypto` contains the canonical implementation of how to hash `secp256k1` keys for use as a peer ID.
|
||||||
|
|
||||||
|
# See Also
|
||||||
|
|
||||||
|
- [multiaddr](https://github.com/multiformats/multiaddr)
|
||||||
|
- [multihash](https://multiformats.io/multihash/)
|
||||||
|
- [go-libp2p-crypto](https://github.com/libp2p/go-libp2p-crypto)
|
|
@ -0,0 +1,292 @@
|
||||||
|
ETH 2.0 Networking Spec - RPC Interface
|
||||||
|
===
|
||||||
|
|
||||||
|
# Abstract
|
||||||
|
|
||||||
|
The Ethereum 2.0 networking stack uses two modes of communication: a broadcast protocol that gossips information to interested parties via GossipSub, and an RPC protocol that retrieves information from specific clients. This specification defines the RPC protocol.
|
||||||
|
|
||||||
|
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL", NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
This specification assumes familiarity with the [Messaging](./messaging.md), [Node Identification](./node-identification), and [Beacon Chain](../core/0_beacon-chain.md) specifications.
|
||||||
|
|
||||||
|
# Specification
|
||||||
|
|
||||||
|
## Message Schemas
|
||||||
|
|
||||||
|
Message body schemas are notated like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
field_name_1: type
|
||||||
|
field_name_2: type
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Embedded types are serialized as SSZ Containers unless otherwise noted.
|
||||||
|
|
||||||
|
All referenced data structures can be found in the [0-beacon-chain](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/0_beacon-chain.md#data-structures) specification.
|
||||||
|
|
||||||
|
## `libp2p` Protocol Names
|
||||||
|
|
||||||
|
A "Protocol ID" in `libp2p` parlance refers to a human-readable identifier `libp2p` uses in order to identify sub-protocols and stream messages of different types over the same connection. Peers exchange supported protocol IDs via the `Identify` protocol upon connection. When opening a new stream, peers pin a particular protocol ID to it, and the stream remains contextualised thereafter. Since messages are sent inside a stream, they do not need to bear the protocol ID.
|
||||||
|
|
||||||
|
## RPC-Over-`libp2p`
|
||||||
|
|
||||||
|
To facilitate RPC-over-`libp2p`, a single protocol name is used: `/eth/serenity/beacon/rpc/1`. The version number in the protocol name is neither backwards or forwards compatible, and will be incremented whenever changes to the below structures are required.
|
||||||
|
|
||||||
|
Remote method calls are wrapped in a "request" structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
id: uint64
|
||||||
|
method_id: uint16
|
||||||
|
body: Request
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
and their corresponding responses are wrapped in a "response" structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
id: uint64
|
||||||
|
response_code: uint16
|
||||||
|
result: bytes
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If an error occurs, a variant of the response structure is returned:
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
id: uint64
|
||||||
|
response_code: uint16
|
||||||
|
result: bytes
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The details of the RPC-Over-`libp2p` protocol are similar to [JSON-RPC 2.0](https://www.jsonrpc.org/specification). Specifically:
|
||||||
|
|
||||||
|
1. The `id` member is REQUIRED.
|
||||||
|
2. The `id` member in the response MUST be the same as the value of the `id` in the request.
|
||||||
|
3. The `id` member MUST be unique within the context of a single connection. Monotonically increasing `id`s are RECOMMENDED.
|
||||||
|
4. The `method_id` member is REQUIRED.
|
||||||
|
5. The `result` member is REQUIRED on success.
|
||||||
|
6. The `result` member is OPTIONAL on errors, and MAY contain additional information about the error.
|
||||||
|
7. `response_code` MUST be `0` on success.
|
||||||
|
|
||||||
|
Structuring RPC requests in this manner allows multiple calls and responses to be multiplexed over the same stream without switching. Note that this implies that responses MAY arrive in a different order than requests.
|
||||||
|
|
||||||
|
The "method ID" fields in the below messages refer to the `method` field in the request structure above.
|
||||||
|
|
||||||
|
The first 1,000 values in `response_code` are reserved for system use. The following response codes are predefined:
|
||||||
|
|
||||||
|
1. `0`: No error.
|
||||||
|
2. `10`: Parse error.
|
||||||
|
2. `20`: Invalid request.
|
||||||
|
3. `30`: Method not found.
|
||||||
|
4. `40`: Server error.
|
||||||
|
|
||||||
|
### Alternative for Non-`libp2p` Clients
|
||||||
|
|
||||||
|
Since some clients are waiting for `libp2p` implementations in their respective languages. As such, they MAY listen for raw TCP messages on port `9000`. To distinguish RPC messages from other messages on that port, a byte prefix of `ETH` (`0x455448`) MUST be prepended to all messages. This option will be removed once `libp2p` is ready in all supported languages.
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
### Hello
|
||||||
|
|
||||||
|
**Method ID:** `0`
|
||||||
|
|
||||||
|
**Body**:
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
network_id: uint8
|
||||||
|
chain_id: uint64
|
||||||
|
latest_finalized_root: bytes32
|
||||||
|
latest_finalized_epoch: uint64
|
||||||
|
best_root: bytes32
|
||||||
|
best_slot: uint64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients exchange `hello` messages upon connection, forming a two-phase handshake. The first message the initiating client sends MUST be the `hello` message. In response, the receiving client MUST respond with its own `hello` message.
|
||||||
|
|
||||||
|
Clients SHOULD immediately disconnect from one another following the handshake above under the following conditions:
|
||||||
|
|
||||||
|
1. If `network_id` belongs to a different chain, since the client definitionally cannot sync with this client.
|
||||||
|
2. If the `latest_finalized_root` shared by the peer is not in the client's chain at the expected epoch. For example, if Peer 1 in the diagram below has `(root, epoch)` of `(A, 5)` and Peer 2 has `(B, 3)`, Peer 1 would disconnect because it knows that `B` is not the root in their chain at epoch 3:
|
||||||
|
|
||||||
|
```
|
||||||
|
Root A
|
||||||
|
|
||||||
|
+---+
|
||||||
|
|xxx| +----+ Epoch 5
|
||||||
|
+-+-+
|
||||||
|
^
|
||||||
|
|
|
||||||
|
+-+-+
|
||||||
|
| | +----+ Epoch 4
|
||||||
|
+-+-+
|
||||||
|
Root B ^
|
||||||
|
|
|
||||||
|
+---+ +-+-+
|
||||||
|
|xxx+<---+--->+ | +----+ Epoch 3
|
||||||
|
+---+ | +---+
|
||||||
|
|
|
||||||
|
+-+-+
|
||||||
|
| | +-----------+ Epoch 2
|
||||||
|
+-+-+
|
||||||
|
^
|
||||||
|
|
|
||||||
|
+-+-+
|
||||||
|
| | +-----------+ Epoch 1
|
||||||
|
+---+
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the handshake completes, the client with the higher `latest_finalized_epoch` or `best_slot` (if the clients have equal `latest_finalized_epoch`s) SHOULD request beacon block roots from its counterparty via `beacon_block_roots` (i.e., RPC method `10`).
|
||||||
|
|
||||||
|
### Goodbye
|
||||||
|
|
||||||
|
**Method ID:** `1`
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
reason: uint64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Client MAY send `goodbye` messages upon disconnection. The reason field MAY be one of the following values:
|
||||||
|
|
||||||
|
- `1`: Client shut down.
|
||||||
|
- `2`: Irrelevant network.
|
||||||
|
- `3`: Fault/error.
|
||||||
|
|
||||||
|
Clients MAY define custom goodbye reasons as long as the value is larger than `1000`.
|
||||||
|
|
||||||
|
### Get Status
|
||||||
|
|
||||||
|
**Method ID:** `2`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
sha: bytes32
|
||||||
|
user_agent: bytes
|
||||||
|
timestamp: uint64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
sha: bytes32
|
||||||
|
user_agent: bytes
|
||||||
|
timestamp: uint64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns metadata about the remote node.
|
||||||
|
|
||||||
|
### Request Beacon Block Roots
|
||||||
|
|
||||||
|
**Method ID:** `10`
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
start_slot: uint64
|
||||||
|
count: uint64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
# BlockRootSlot
|
||||||
|
(
|
||||||
|
block_root: bytes32
|
||||||
|
slot: uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
roots: []BlockRootSlot
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Requests a list of block roots and slots from the peer. The `count` parameter MUST be less than or equal to `32768`. The slots MUST be returned in ascending slot order.
|
||||||
|
|
||||||
|
### Beacon Block Headers
|
||||||
|
|
||||||
|
**Method ID:** `11`
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
start_root: HashTreeRoot
|
||||||
|
start_slot: uint64
|
||||||
|
max_headers: uint64
|
||||||
|
skip_slots: uint64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
headers: []BeaconBlockHeader
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Requests beacon block headers from the peer starting from `(start_root, start_slot)`. The response MUST contain no more than `max_headers` headers. `skip_slots` defines the maximum number of slots to skip between blocks. For example, requesting blocks starting at slots `2` a `skip_slots` value of `1` would return the blocks at `[2, 4, 6, 8, 10]`. In cases where a slot is empty for a given slot number, the closest previous block MUST be returned. For example, if slot `4` were empty in the previous example, the returned array would contain `[2, 3, 6, 8, 10]`. If slot three were further empty, the array would contain `[2, 6, 8, 10]` - i.e., duplicate blocks MUST be collapsed. A `skip_slots` value of `0` returns all blocks.
|
||||||
|
|
||||||
|
The function of the `skip_slots` parameter helps facilitate light client sync - for example, in [#459](https://github.com/ethereum/eth2.0-specs/issues/459) - and allows clients to balance the peers from whom they request headers. Clients could, for instance, request every 10th block from a set of peers where each per has a different starting block in order to populate block data.
|
||||||
|
|
||||||
|
### Beacon Block Bodies
|
||||||
|
|
||||||
|
**Method ID:** `12`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
block_roots: []HashTreeRoot
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
block_bodies: []BeaconBlockBody
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Requests the `block_bodies` associated with the provided `block_roots` from the peer. Responses MUST return `block_roots` in the order provided in the request. If the receiver does not have a particular `block_root`, it must return a zero-value `block_body` (i.e., a `block_body` container with all zero fields).
|
||||||
|
|
||||||
|
### Beacon Chain State
|
||||||
|
|
||||||
|
**Note:** This section is preliminary, pending the definition of the data structures to be transferred over the wire during fast sync operations.
|
||||||
|
|
||||||
|
**Method ID:** `13`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```
|
||||||
|
(
|
||||||
|
hashes: []HashTreeRoot
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:** TBD
|
||||||
|
|
||||||
|
Requests contain the hashes of Merkle tree nodes that when merkelized yield the block's `state_root`.
|
||||||
|
|
||||||
|
The response will contain the values that, when hashed, yield the hashes inside the request body.
|
|
@ -12,7 +12,7 @@ This is a **work in progress** describing typing, serialization and Merkleizatio
|
||||||
- [Serialization](#serialization)
|
- [Serialization](#serialization)
|
||||||
- [`"uintN"`](#uintn)
|
- [`"uintN"`](#uintn)
|
||||||
- [`"bool"`](#bool)
|
- [`"bool"`](#bool)
|
||||||
- [Tuples, containers, lists](#tuples-containers-lists)
|
- [Vectors, containers, lists](#vectors-containers-lists)
|
||||||
- [Deserialization](#deserialization)
|
- [Deserialization](#deserialization)
|
||||||
- [Merkleization](#merkleization)
|
- [Merkleization](#merkleization)
|
||||||
- [Self-signed containers](#self-signed-containers)
|
- [Self-signed containers](#self-signed-containers)
|
||||||
|
@ -34,12 +34,14 @@ This is a **work in progress** describing typing, serialization and Merkleizatio
|
||||||
### Composite types
|
### Composite types
|
||||||
|
|
||||||
* **container**: ordered heterogenous collection of values
|
* **container**: ordered heterogenous collection of values
|
||||||
* key-pair curly bracket notation `{}`, e.g. `{'foo': "uint64", 'bar': "bool"}`
|
* key-pair curly bracket notation `{}`, e.g. `{"foo": "uint64", "bar": "bool"}`
|
||||||
* **tuple**: ordered fixed-length homogeneous collection of values
|
* **vector**: ordered fixed-length homogeneous collection of values
|
||||||
* angle bracket notation `[type, N]`, e.g. `["uint64", N]`
|
* angle bracket notation `[type, N]`, e.g. `["uint64", N]`
|
||||||
* **list**: ordered variable-length homogenous collection of values
|
* **list**: ordered variable-length homogenous collection of values
|
||||||
* angle bracket notation `[type]`, e.g. `["uint64"]`
|
* angle bracket notation `[type]`, e.g. `["uint64"]`
|
||||||
|
|
||||||
|
We recursively define "variable-size" types to be lists and all types that contains a variable-size type. All other types are said to be "fixed-size".
|
||||||
|
|
||||||
### Aliases
|
### Aliases
|
||||||
|
|
||||||
For convenience we alias:
|
For convenience we alias:
|
||||||
|
@ -54,34 +56,34 @@ We recursively define the `serialize` function which consumes an object `value`
|
||||||
|
|
||||||
*Note*: In the function definitions below (`serialize`, `hash_tree_root`, `signed_root`, etc.) objects implicitly carry their type.
|
*Note*: In the function definitions below (`serialize`, `hash_tree_root`, `signed_root`, etc.) objects implicitly carry their type.
|
||||||
|
|
||||||
### `uintN`
|
### `"uintN"`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
assert N in [8, 16, 32, 64, 128, 256]
|
assert N in [8, 16, 32, 64, 128, 256]
|
||||||
return value.to_bytes(N // 8, 'little')
|
return value.to_bytes(N // 8, "little")
|
||||||
```
|
```
|
||||||
|
|
||||||
### `bool`
|
### `"bool"`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
assert value in (True, False)
|
assert value in (True, False)
|
||||||
return b'\x01' if value is True else b'\x00'
|
return b"\x01" if value is True else b"\x00"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tuples, containers, lists
|
### Vectors, containers, lists
|
||||||
|
|
||||||
If `value` is fixed-length (i.e. does not embed a list):
|
If `value` is fixed-size:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
return ''.join([serialize(element) for element in value])
|
return "".join([serialize(element) for element in value])
|
||||||
```
|
```
|
||||||
|
|
||||||
If `value` is variable-length (i.e. embeds a list):
|
If `value` is variable-size:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
serialized_bytes = ''.join([serialize(element) for element in value])
|
serialized_bytes = "".join([serialize(element) for element in value])
|
||||||
assert len(serialized_bytes) < 2**(8 * BYTES_PER_LENGTH_PREFIX)
|
assert len(serialized_bytes) < 2**(8 * BYTES_PER_LENGTH_PREFIX)
|
||||||
serialized_length = len(serialized_bytes).to_bytes(BYTES_PER_LENGTH_PREFIX, 'little')
|
serialized_length = len(serialized_bytes).to_bytes(BYTES_PER_LENGTH_PREFIX, "little")
|
||||||
return serialized_length + serialized_bytes
|
return serialized_length + serialized_bytes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -99,9 +101,9 @@ We first define helper functions:
|
||||||
|
|
||||||
We now define Merkleization `hash_tree_root(value)` of an object `value` recursively:
|
We now define Merkleization `hash_tree_root(value)` of an object `value` recursively:
|
||||||
|
|
||||||
* `merkleize(pack(value))` if `value` is a basic object or a tuple of basic objects
|
* `merkleize(pack(value))` if `value` is a basic object or a vector of basic objects
|
||||||
* `mix_in_length(merkleize(pack(value)), len(value))` if `value` is a list of basic objects
|
* `mix_in_length(merkleize(pack(value)), len(value))` if `value` is a list of basic objects
|
||||||
* `merkleize([hash_tree_root(element) for element in value])` if `value` is a tuple of composite objects or a container
|
* `merkleize([hash_tree_root(element) for element in value])` if `value` is a vector of composite objects or a container
|
||||||
* `mix_in_length(merkleize([hash_tree_root(element) for element in value]), len(value))` if `value` is a list of composite objects
|
* `mix_in_length(merkleize([hash_tree_root(element) for element in value]), len(value))` if `value` is a list of composite objects
|
||||||
|
|
||||||
## Self-signed containers
|
## Self-signed containers
|
||||||
|
|
|
@ -40,11 +40,11 @@ __NOTICE__: This document is a work-in-progress for researchers and implementers
|
||||||
- [Slot](#slot-1)
|
- [Slot](#slot-1)
|
||||||
- [Shard](#shard)
|
- [Shard](#shard)
|
||||||
- [Beacon block root](#beacon-block-root)
|
- [Beacon block root](#beacon-block-root)
|
||||||
- [Epoch boundary root](#epoch-boundary-root)
|
- [Target root](#target-root)
|
||||||
- [Crosslink data root](#crosslink-data-root)
|
- [Crosslink data root](#crosslink-data-root)
|
||||||
- [Latest crosslink](#latest-crosslink)
|
- [Latest crosslink](#latest-crosslink)
|
||||||
- [Justified epoch](#justified-epoch)
|
- [Source epoch](#source-epoch)
|
||||||
- [Justified block root](#justified-block-root)
|
- [Source root](#source-root)
|
||||||
- [Construct attestation](#construct-attestation)
|
- [Construct attestation](#construct-attestation)
|
||||||
- [Data](#data)
|
- [Data](#data)
|
||||||
- [Aggregation bitfield](#aggregation-bitfield)
|
- [Aggregation bitfield](#aggregation-bitfield)
|
||||||
|
@ -101,8 +101,7 @@ In phase 0, all incoming validator deposits originate from the Ethereum 1.0 PoW
|
||||||
To submit a deposit:
|
To submit a deposit:
|
||||||
|
|
||||||
* Pack the validator's [initialization parameters](#initialization) into `deposit_input`, a [`DepositInput`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#depositinput) SSZ object.
|
* Pack the validator's [initialization parameters](#initialization) into `deposit_input`, a [`DepositInput`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#depositinput) SSZ object.
|
||||||
* Set `deposit_input.proof_of_possession = EMPTY_SIGNATURE`.
|
* Let `proof_of_possession` be the result of `bls_sign` of the `signed_root(deposit_input)` with `domain=DOMAIN_DEPOSIT`.
|
||||||
* Let `proof_of_possession` be the result of `bls_sign` of the `hash_tree_root(deposit_input)` with `domain=DOMAIN_DEPOSIT`.
|
|
||||||
* Set `deposit_input.proof_of_possession = proof_of_possession`.
|
* Set `deposit_input.proof_of_possession = proof_of_possession`.
|
||||||
* Let `amount` be the amount in Gwei to be deposited by the validator where `MIN_DEPOSIT_AMOUNT <= amount <= MAX_DEPOSIT_AMOUNT`.
|
* Let `amount` be the amount in Gwei to be deposited by the validator where `MIN_DEPOSIT_AMOUNT <= amount <= MAX_DEPOSIT_AMOUNT`.
|
||||||
* Send a transaction on the Ethereum 1.0 chain to `DEPOSIT_CONTRACT_ADDRESS` executing `deposit` along with `serialize(deposit_input)` as the singular `bytes` input along with a deposit `amount` in Gwei.
|
* Send a transaction on the Ethereum 1.0 chain to `DEPOSIT_CONTRACT_ADDRESS` executing `deposit` along with `serialize(deposit_input)` as the singular `bytes` input along with a deposit `amount` in Gwei.
|
||||||
|
@ -121,11 +120,12 @@ Once a validator has been processed and added to the beacon state's `validator_r
|
||||||
|
|
||||||
In normal operation, the validator is quickly activated at which point the validator is added to the shuffling and begins validation after an additional `ACTIVATION_EXIT_DELAY` epochs (25.6 minutes).
|
In normal operation, the validator is quickly activated at which point the validator is added to the shuffling and begins validation after an additional `ACTIVATION_EXIT_DELAY` epochs (25.6 minutes).
|
||||||
|
|
||||||
The function [`is_active_validator`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#is_active_validator) can be used to check if a validator is active during a given epoch. Usage is as follows:
|
The function [`is_active_validator`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#is_active_validator) can be used to check if a validator is active during a given shuffling epoch. Note that the `BeaconState` contains a field `current_shuffling_epoch` which dictates from which epoch the current active validators are taken. Usage is as follows:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
shuffling_epoch = state.current_shuffling_epoch
|
||||||
validator = state.validator_registry[validator_index]
|
validator = state.validator_registry[validator_index]
|
||||||
is_active = is_active_validator(validator, epoch)
|
is_active = is_active_validator(validator, shuffling_epoch)
|
||||||
```
|
```
|
||||||
|
|
||||||
Once a validator is activated, the validator is assigned [responsibilities](#beacon-chain-responsibilities) until exited.
|
Once a validator is activated, the validator is assigned [responsibilities](#beacon-chain-responsibilities) until exited.
|
||||||
|
@ -138,7 +138,7 @@ A validator has two primary responsibilities to the beacon chain -- [proposing b
|
||||||
|
|
||||||
### Block proposal
|
### Block proposal
|
||||||
|
|
||||||
A validator is expected to propose a [`BeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beaconblock) at the beginning of any slot during which `get_beacon_proposer_index(state, slot)` returns the validator's `validator_index`. To propose, the validator selects the `BeaconBlock`, `parent`, that in their view of the fork choice is the head of the chain during `slot`. The validator is to create, sign, and broadcast a `block` that is a child of `parent` and that executes a valid [beacon chain state transition](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beacon-chain-state-transition-function).
|
A validator is expected to propose a [`BeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beaconblock) at the beginning of any slot during which `get_beacon_proposer_index(state, slot)` returns the validator's `validator_index`. To propose, the validator selects the `BeaconBlock`, `parent`, that in their view of the fork choice is the head of the chain during `slot - 1`. The validator is to create, sign, and broadcast a `block` that is a child of `parent` and that executes a valid [beacon chain state transition](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beacon-chain-state-transition-function).
|
||||||
|
|
||||||
There is one proposer per slot, so if there are N active validators any individual validator will on average be assigned to propose once per N slots (eg. at 312500 validators = 10 million ETH, that's once per ~3 weeks).
|
There is one proposer per slot, so if there are N active validators any individual validator will on average be assigned to propose once per N slots (eg. at 312500 validators = 10 million ETH, that's once per ~3 weeks).
|
||||||
|
|
||||||
|
@ -152,13 +152,13 @@ _Note:_ there might be "skipped" slots between the `parent` and `block`. These s
|
||||||
|
|
||||||
##### Parent root
|
##### Parent root
|
||||||
|
|
||||||
Set `block.parent_root = hash_tree_root(parent)`.
|
Set `block.previous_block_root = signed_root(parent)`.
|
||||||
|
|
||||||
##### State root
|
##### State root
|
||||||
|
|
||||||
Set `block.state_root = hash_tree_root(state)` of the resulting `state` of the `parent -> block` state transition.
|
Set `block.state_root = hash_tree_root(state)` of the resulting `state` of the `parent -> block` state transition.
|
||||||
|
|
||||||
_Note_: To calculate `state_root`, the validator should first run the state transition function on an unsigned `block` containing a stub for the `state_root`. It is useful to be able to run a state transition function that does _not_ validate signatures for this purpose.
|
_Note_: To calculate `state_root`, the validator should first run the state transition function on an unsigned `block` containing a stub for the `state_root`. It is useful to be able to run a state transition function that does _not_ validate signatures or state root for this purpose.
|
||||||
|
|
||||||
##### Randao reveal
|
##### Randao reveal
|
||||||
|
|
||||||
|
@ -166,8 +166,8 @@ Set `block.randao_reveal = epoch_signature` where `epoch_signature` is defined a
|
||||||
|
|
||||||
```python
|
```python
|
||||||
epoch_signature = bls_sign(
|
epoch_signature = bls_sign(
|
||||||
privkey=validator.privkey, # privkey store locally, not in state
|
privkey=validator.privkey, # privkey stored locally, not in state
|
||||||
message_hash=int_to_bytes32(slot_to_epoch(block.slot)),
|
message_hash=hash_tree_root(slot_to_epoch(block.slot)),
|
||||||
domain=get_domain(
|
domain=get_domain(
|
||||||
fork=fork, # `fork` is the fork object at the slot `block.slot`
|
fork=fork, # `fork` is the fork object at the slot `block.slot`
|
||||||
epoch=slot_to_epoch(block.slot),
|
epoch=slot_to_epoch(block.slot),
|
||||||
|
@ -182,35 +182,28 @@ epoch_signature = bls_sign(
|
||||||
|
|
||||||
* Let `D` be the set of `Eth1DataVote` objects `vote` in `state.eth1_data_votes` where:
|
* Let `D` be the set of `Eth1DataVote` objects `vote` in `state.eth1_data_votes` where:
|
||||||
* `vote.eth1_data.block_hash` is the hash of an eth1.0 block that is (i) part of the canonical chain, (ii) >= `ETH1_FOLLOW_DISTANCE` blocks behind the head, and (iii) newer than `state.latest_eth1_data.block_data`.
|
* `vote.eth1_data.block_hash` is the hash of an eth1.0 block that is (i) part of the canonical chain, (ii) >= `ETH1_FOLLOW_DISTANCE` blocks behind the head, and (iii) newer than `state.latest_eth1_data.block_data`.
|
||||||
|
* `vote.eth1_data.deposit_count` is the deposit count of the eth1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`.
|
||||||
* `vote.eth1_data.deposit_root` is the deposit root of the eth1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`.
|
* `vote.eth1_data.deposit_root` is the deposit root of the eth1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`.
|
||||||
* If `D` is empty:
|
* If `D` is empty:
|
||||||
* Let `block_hash` be the block hash of the `ETH1_FOLLOW_DISTANCE`'th ancestor of the head of the canonical eth1.0 chain.
|
* Let `block_hash` be the block hash of the `ETH1_FOLLOW_DISTANCE`'th ancestor of the head of the canonical eth1.0 chain.
|
||||||
* Let `deposit_root` be the deposit root of the eth1.0 deposit contract in the post-state of the block referenced by `block_hash`
|
* Let `deposit_root` and `deposit_count` be the deposit root and deposit count of the eth1.0 deposit contract in the post-state of the block referenced by `block_hash`
|
||||||
|
* Let `best_vote_data = Eth1Data(block_hash=block_hash, deposit_root=deposit_root, deposit_count=deposit_count)`.
|
||||||
* If `D` is nonempty:
|
* If `D` is nonempty:
|
||||||
* Let `best_vote` be the member of `D` that has the highest `vote.vote_count`, breaking ties by favoring block hashes with higher associated block height.
|
* Let `best_vote_data` be the `eth1_data` of the member of `D` that has the highest `vote.vote_count`, breaking ties by favoring block hashes with higher associated block height.
|
||||||
* Let `block_hash = best_vote.eth1_data.block_hash`.
|
* Set `block.eth1_data = best_vote_data`.
|
||||||
* Let `deposit_root = best_vote.eth1_data.deposit_root`.
|
|
||||||
* Set `block.eth1_data = Eth1Data(deposit_root=deposit_root, block_hash=block_hash)`.
|
|
||||||
|
|
||||||
##### Signature
|
##### Signature
|
||||||
|
|
||||||
Set `block.signature = signed_proposal_data` where `signed_proposal_data` is defined as:
|
Set `block.signature = block_signature` where `block_signature` is defined as:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
proposal_data = ProposalSignedData(
|
block_signature = bls_sign(
|
||||||
slot=slot,
|
|
||||||
shard=BEACON_CHAIN_SHARD_NUMBER,
|
|
||||||
block_root=hash_tree_root(block), # where `block.sigature == EMPTY_SIGNATURE
|
|
||||||
)
|
|
||||||
proposal_root = hash_tree_root(proposal_data)
|
|
||||||
|
|
||||||
signed_proposal_data = bls_sign(
|
|
||||||
privkey=validator.privkey, # privkey store locally, not in state
|
privkey=validator.privkey, # privkey store locally, not in state
|
||||||
message_hash=proposal_root,
|
message_hash=signed_root(block),
|
||||||
domain=get_domain(
|
domain=get_domain(
|
||||||
fork=fork, # `fork` is the fork object at the slot `block.slot`
|
fork=fork, # `fork` is the fork object at the slot `block.slot`
|
||||||
epoch=slot_to_epoch(block.slot),
|
epoch=slot_to_epoch(block.slot),
|
||||||
domain_type=DOMAIN_PROPOSAL,
|
domain_type=DOMAIN_BEACON_BLOCK,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -219,23 +212,25 @@ signed_proposal_data = bls_sign(
|
||||||
|
|
||||||
##### Proposer slashings
|
##### Proposer slashings
|
||||||
|
|
||||||
Up to `MAX_PROPOSER_SLASHINGS` [`ProposerSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposerslashing) objects can be included in the `block`. The proposer slashings must satisfy the verification conditions found in [proposer slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposer-slashings-1). The validator receives a small "whistleblower" reward for each proposer slashing found and included.
|
Up to `MAX_PROPOSER_SLASHINGS` [`ProposerSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposerslashing) objects can be included in the `block`. The proposer slashings must satisfy the verification conditions found in [proposer slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposer-slashings). The validator receives a small "whistleblower" reward for each proposer slashing found and included.
|
||||||
|
|
||||||
##### Attester slashings
|
##### Attester slashings
|
||||||
|
|
||||||
Up to `MAX_ATTESTER_SLASHINGS` [`AttesterSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attesterslashing) objects can be included in the `block`. The attester slashings must satisfy the verification conditions found in [Attester slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attester-slashings-1). The validator receives a small "whistleblower" reward for each attester slashing found and included.
|
Up to `MAX_ATTESTER_SLASHINGS` [`AttesterSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attesterslashing) objects can be included in the `block`. The attester slashings must satisfy the verification conditions found in [Attester slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attester-slashings). The validator receives a small "whistleblower" reward for each attester slashing found and included.
|
||||||
|
|
||||||
##### Attestations
|
##### Attestations
|
||||||
|
|
||||||
Up to `MAX_ATTESTATIONS` aggregate attestations can be included in the `block`. The attestations added must satisfy the verification conditions found in [attestation processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestations-1). To maximize profit, the validator should attempt to create aggregate attestations that include singular attestations from the largest number of validators whose signatures from the same epoch have not previously been added on chain.
|
Up to `MAX_ATTESTATIONS` aggregate attestations can be included in the `block`. The attestations added must satisfy the verification conditions found in [attestation processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestations). To maximize profit, the validator should attempt to gather aggregate attestations that include singular attestations from the largest number of validators whose signatures from the same epoch have not previously been added on chain.
|
||||||
|
|
||||||
##### Deposits
|
##### Deposits
|
||||||
|
|
||||||
Up to `MAX_DEPOSITS` [`Deposit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposit) objects can be included in the `block`. These deposits are constructed from the `Deposit` logs from the [Eth1.0 deposit contract](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#ethereum-10-deposit-contract) and must be processed in sequential order. The deposits included in the `block` must satisfy the verification conditions found in [deposits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposits-1).
|
If there are any unprocessed deposits for the existing `state.latest_eth1_data` (i.e. `state.latest_eth1_data.deposit_count > state.deposit_index`), then pending deposits _must_ be added to the block. The expected number of deposits is exactly `min(MAX_DEPOSITS, latest_eth1_data.deposit_count - state.deposit_index)`. These [`deposits`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposit) are constructed from the `Deposit` logs from the [Eth1.0 deposit contract](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#ethereum-10-deposit-contract) and must be processed in sequential order. The deposits included in the `block` must satisfy the verification conditions found in [deposits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposits).
|
||||||
|
|
||||||
|
The `proof` for each deposit must be constructed against the deposit root contained in `state.latest_eth1_data` rather than the deposit root at the time the deposit was initially logged from the 1.0 chain. This entails storing a full deposit merkle tree locally and computing updated proofs against the `latest_eth1_data.deposit_root` as needed. See [`minimal_merkle.py`](https://github.com/ethereum/research/blob/master/spec_pythonizer/utils/merkle_minimal.py) for a sample implementation.
|
||||||
|
|
||||||
##### Voluntary exits
|
##### Voluntary exits
|
||||||
|
|
||||||
Up to `MAX_VOLUNTARY_EXITS` [`VoluntaryExit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#voluntaryexit) objects can be included in the `block`. The exits must satisfy the verification conditions found in [exits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#exits-1).
|
Up to `MAX_VOLUNTARY_EXITS` [`VoluntaryExit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#voluntaryexit) objects can be included in the `block`. The exits must satisfy the verification conditions found in [exits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#voluntary-exits).
|
||||||
|
|
||||||
### Attestations
|
### Attestations
|
||||||
|
|
||||||
|
@ -247,9 +242,12 @@ A validator should create and broadcast the attestation halfway through the `slo
|
||||||
|
|
||||||
First the validator should construct `attestation_data`, an [`AttestationData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestationdata) object based upon the state at the assigned slot.
|
First the validator should construct `attestation_data`, an [`AttestationData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestationdata) object based upon the state at the assigned slot.
|
||||||
|
|
||||||
|
* Let `head_block` be the result of running the fork choice during the assigned slot.
|
||||||
|
* Let `head_state` be the state of `head_block` processed through any empty slots up to the assigned slot.
|
||||||
|
|
||||||
##### Slot
|
##### Slot
|
||||||
|
|
||||||
Set `attestation_data.slot = slot` where `slot` is the current slot of which the validator is a member of a committee.
|
Set `attestation_data.slot = head_state.slot`.
|
||||||
|
|
||||||
##### Shard
|
##### Shard
|
||||||
|
|
||||||
|
@ -257,15 +255,15 @@ Set `attestation_data.shard = shard` where `shard` is the shard associated with
|
||||||
|
|
||||||
##### Beacon block root
|
##### Beacon block root
|
||||||
|
|
||||||
Set `attestation_data.beacon_block_root = hash_tree_root(head)` where `head` is the validator's view of the `head` block of the beacon chain during `slot`.
|
Set `attestation_data.beacon_block_root = signed_root(head_block)`.
|
||||||
|
|
||||||
##### Epoch boundary root
|
##### Target root
|
||||||
|
|
||||||
Set `attestation_data.epoch_boundary_root = hash_tree_root(epoch_boundary)` where `epoch_boundary` is the block at the most recent epoch boundary in the chain defined by `head` -- i.e. the `BeaconBlock` where `block.slot == get_epoch_start_slot(slot_to_epoch(head.slot))`.
|
Set `attestation_data.target_root = signed_root(epoch_boundary)` where `epoch_boundary` is the block at the most recent epoch boundary.
|
||||||
|
|
||||||
_Note:_ This can be looked up in the state using:
|
_Note:_ This can be looked up in the state using:
|
||||||
* Let `epoch_start_slot = get_epoch_start_slot(slot_to_epoch(head.slot))`.
|
* Let `epoch_start_slot = get_epoch_start_slot(get_current_epoch(head_state))`.
|
||||||
* Set `epoch_boundary_root = hash_tree_root(head) if epoch_start_slot == head.slot else get_block_root(state, epoch_start_slot)`.
|
* Set `epoch_boundary = head if epoch_start_slot == head_state.slot else get_block_root(state, epoch_start_slot)`.
|
||||||
|
|
||||||
##### Crosslink data root
|
##### Crosslink data root
|
||||||
|
|
||||||
|
@ -275,17 +273,15 @@ _Note:_ This is a stub for phase 0.
|
||||||
|
|
||||||
##### Latest crosslink
|
##### Latest crosslink
|
||||||
|
|
||||||
Set `attestation_data.latest_crosslink = state.latest_crosslinks[shard]` where `state` is the beacon state at `head` and `shard` is the validator's assigned shard.
|
Set `attestation_data.previous_crosslink = head_state.latest_crosslinks[shard]`.
|
||||||
|
|
||||||
##### Justified epoch
|
##### Source epoch
|
||||||
|
|
||||||
Set `attestation_data.justified_epoch = state.justified_epoch` where `state` is the beacon state at `head`.
|
Set `attestation_data.source_epoch = head_state.justified_epoch`.
|
||||||
|
|
||||||
##### Justified block root
|
##### Source root
|
||||||
|
|
||||||
Set `attestation_data.justified_block_root = hash_tree_root(justified_block)` where `justified_block` is the block at the slot `get_epoch_start_slot(state.justified_epoch)` in the chain defined by `head`.
|
Set `attestation_data.source_root = head_state.current_justified_root`.
|
||||||
|
|
||||||
_Note:_ This can be looked up in the state using `get_block_root(state, get_epoch_start_slot(state.justified_epoch))`.
|
|
||||||
|
|
||||||
#### Construct attestation
|
#### Construct attestation
|
||||||
|
|
||||||
|
@ -320,11 +316,11 @@ attestation_data_and_custody_bit = AttestationDataAndCustodyBit(
|
||||||
data=attestation.data,
|
data=attestation.data,
|
||||||
custody_bit=0b0,
|
custody_bit=0b0,
|
||||||
)
|
)
|
||||||
attestation_message_to_sign = hash_tree_root(attestation_data_and_custody_bit)
|
attestation_message = hash_tree_root(attestation_data_and_custody_bit)
|
||||||
|
|
||||||
signed_attestation_data = bls_sign(
|
signed_attestation_data = bls_sign(
|
||||||
privkey=validator.privkey, # privkey store locally, not in state
|
privkey=validator.privkey, # privkey stored locally, not in state
|
||||||
message_hash=attestation_message_to_sign,
|
message_hash=attestation_message,
|
||||||
domain=get_domain(
|
domain=get_domain(
|
||||||
fork=fork, # `fork` is the fork object at the slot, `attestation_data.slot`
|
fork=fork, # `fork` is the fork object at the slot, `attestation_data.slot`
|
||||||
epoch=slot_to_epoch(attestation_data.slot),
|
epoch=slot_to_epoch(attestation_data.slot),
|
||||||
|
@ -335,22 +331,19 @@ signed_attestation_data = bls_sign(
|
||||||
|
|
||||||
## Validator assignments
|
## Validator assignments
|
||||||
|
|
||||||
A validator can get the current and previous epoch committee assignments using the following helper via `get_committee_assignment(state, epoch, validator_index)` where `previous_epoch <= epoch <= current_epoch`.
|
A validator can get the current, previous, and next epoch committee assignments using the following helper via `get_committee_assignment(state, epoch, validator_index)` where `previous_epoch <= epoch <= next_epoch`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def get_committee_assignment(
|
def get_committee_assignment(
|
||||||
state: BeaconState,
|
state: BeaconState,
|
||||||
epoch: Epoch,
|
epoch: Epoch,
|
||||||
validator_index: ValidatorIndex,
|
validator_index: ValidatorIndex) -> Tuple[List[ValidatorIndex], Shard, Slot]:
|
||||||
registry_change: bool=False) -> Tuple[List[ValidatorIndex], Shard, Slot, bool]:
|
|
||||||
"""
|
"""
|
||||||
Return the committee assignment in the ``epoch`` for ``validator_index`` and ``registry_change``.
|
Return the committee assignment in the ``epoch`` for ``validator_index``.
|
||||||
``assignment`` returned is a tuple of the following form:
|
``assignment`` returned is a tuple of the following form:
|
||||||
* ``assignment[0]`` is the list of validators in the committee
|
* ``assignment[0]`` is the list of validators in the committee
|
||||||
* ``assignment[1]`` is the shard to which the committee is assigned
|
* ``assignment[1]`` is the shard to which the committee is assigned
|
||||||
* ``assignment[2]`` is the slot at which the committee is assigned
|
* ``assignment[2]`` is the slot at which the committee is assigned
|
||||||
* ``assignment[3]`` is a bool signalling if the validator is expected to propose
|
|
||||||
a beacon block at the assigned slot.
|
|
||||||
"""
|
"""
|
||||||
previous_epoch = get_previous_epoch(state)
|
previous_epoch = get_previous_epoch(state)
|
||||||
next_epoch = get_current_epoch(state) + 1
|
next_epoch = get_current_epoch(state) + 1
|
||||||
|
@ -361,7 +354,6 @@ def get_committee_assignment(
|
||||||
crosslink_committees = get_crosslink_committees_at_slot(
|
crosslink_committees = get_crosslink_committees_at_slot(
|
||||||
state,
|
state,
|
||||||
slot,
|
slot,
|
||||||
registry_change=registry_change,
|
|
||||||
)
|
)
|
||||||
selected_committees = [
|
selected_committees = [
|
||||||
committee # Tuple[List[ValidatorIndex], Shard]
|
committee # Tuple[List[ValidatorIndex], Shard]
|
||||||
|
@ -371,28 +363,33 @@ def get_committee_assignment(
|
||||||
if len(selected_committees) > 0:
|
if len(selected_committees) > 0:
|
||||||
validators = selected_committees[0][0]
|
validators = selected_committees[0][0]
|
||||||
shard = selected_committees[0][1]
|
shard = selected_committees[0][1]
|
||||||
is_proposer = validator_index == get_beacon_proposer_index(state, slot, registry_change=registry_change)
|
|
||||||
|
|
||||||
assignment = (validators, shard, slot, is_proposer)
|
assignment = (validators, shard, slot)
|
||||||
return assignment
|
return assignment
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A validator can use the following function to see if they are supposed to propose during their assigned committee slot. This function can only be run during the epoch of the slot in question and can not reliably be used to predict an epoch in advance.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def is_proposer_at_slot(state: BeaconState,
|
||||||
|
slot: Slot,
|
||||||
|
validator_index: ValidatorIndex) -> bool:
|
||||||
|
current_epoch = get_current_epoch(state)
|
||||||
|
assert slot_to_epoch(slot) == current_epoch
|
||||||
|
|
||||||
|
return get_beacon_proposer_index(state, slot) == validator_index
|
||||||
|
```
|
||||||
|
|
||||||
|
_Note_: If a validator is assigned to the 0th slot of an epoch, the validator must run an empty slot transition from the previous epoch into the 0th slot of the epoch to be able to check if they are a proposer at that slot.
|
||||||
|
|
||||||
|
|
||||||
### Lookahead
|
### Lookahead
|
||||||
|
|
||||||
The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead on the validator's upcoming assignments of proposing and attesting dictated by the shuffling and slot.
|
The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead on the validator's upcoming committee assignments for attesting dictated by the shuffling and slot. Note that this lookahead does not apply to proposing which must checked during the epoch in question.
|
||||||
|
|
||||||
There are three possibilities for the shuffling at the next epoch:
|
`get_committee_assignment` should be called at the start of each epoch to get the assignment for the next epoch (`current_epoch + 1`). A validator should plan for future assignments which involves noting at which future slot one will have to attest and also which shard one should begin syncing (in phase 1+).
|
||||||
1. The shuffling changes due to a "validator registry change".
|
|
||||||
2. The shuffling changes due to `epochs_since_last_registry_update` being an exact power of 2 greater than 1.
|
|
||||||
3. The shuffling remains the same (i.e. the validator is in the same shard committee).
|
|
||||||
|
|
||||||
Either (2) or (3) occurs if (1) fails. The choice between (2) and (3) is deterministic based upon `epochs_since_last_registry_update`.
|
Specifically, a validator should call `get_committee_assignment(state, next_epoch, validator_index)` when checking for next epoch assignments.
|
||||||
|
|
||||||
When querying for assignments in the next epoch there are two options -- with and without a `registry_change` -- which is the optional fourth parameter of the `get_committee_assignment`.
|
|
||||||
|
|
||||||
`get_committee_assignment` should be called at the start of each epoch to get the assignment for the next epoch (`current_epoch + 1`). A validator should always plan for assignments from both values of `registry_change` unless the validator can concretely eliminate one of the options. Planning for future assignments involves noting at which future slot one might have to attest and propose and also which shard one should begin syncing (in phase 1+).
|
|
||||||
|
|
||||||
Specifically, a validator should call both `get_committee_assignment(state, next_epoch, validator_index, registry_change=True)` and `get_committee_assignment(state, next_epoch, validator_index, registry_change=False)` when checking for next epoch assignments.
|
|
||||||
|
|
||||||
## How to avoid slashing
|
## How to avoid slashing
|
||||||
|
|
||||||
|
@ -402,12 +399,12 @@ _Note_: Signed data must be within a sequential `Fork` context to conflict. Mess
|
||||||
|
|
||||||
### Proposer slashing
|
### Proposer slashing
|
||||||
|
|
||||||
To avoid "proposer slashings", a validator must not sign two conflicting [`ProposalSignedData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposalsigneddata) where conflicting is defined as having the same `slot` and `shard` but a different `block_root`. In phase 0, proposals are only made for the beacon chain (`shard == BEACON_CHAIN_SHARD_NUMBER`).
|
To avoid "proposer slashings", a validator must not sign two conflicting [`BeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposalsigneddata) where conflicting is defined as two distinct blocks within the same epoch.
|
||||||
|
|
||||||
_In phase 0, as long as the validator does not sign two different beacon chain proposals for the same slot, the validator is safe against proposer slashings._
|
_In phase 0, as long as the validator does not sign two different beacon blocks for the same epoch, the validator is safe against proposer slashings._
|
||||||
|
|
||||||
Specifically, when signing an `BeaconBlock`, a validator should perform the following steps in the following order:
|
Specifically, when signing an `BeaconBlock`, a validator should perform the following steps in the following order:
|
||||||
1. Save a record to hard disk that an beacon block has been signed for the `slot=slot` and `shard=BEACON_CHAIN_SHARD_NUMBER`.
|
1. Save a record to hard disk that an beacon block has been signed for the `epoch=slot_to_epoch(block.slot)`.
|
||||||
2. Generate and broadcast the block.
|
2. Generate and broadcast the block.
|
||||||
|
|
||||||
If the software crashes at some point within this routine, then when the validator comes back online the hard disk has the record of the _potentially_ signed/broadcast block and can effectively avoid slashing.
|
If the software crashes at some point within this routine, then when the validator comes back online the hard disk has the record of the _potentially_ signed/broadcast block and can effectively avoid slashing.
|
||||||
|
@ -417,7 +414,7 @@ If the software crashes at some point within this routine, then when the validat
|
||||||
To avoid "attester slashings", a validator must not sign two conflicting [`AttestationData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestationdata) objects where conflicting is defined as a set of two attestations that satisfy either [`is_double_vote`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#is_double_vote) or [`is_surround_vote`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#is_surround_vote).
|
To avoid "attester slashings", a validator must not sign two conflicting [`AttestationData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestationdata) objects where conflicting is defined as a set of two attestations that satisfy either [`is_double_vote`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#is_double_vote) or [`is_surround_vote`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#is_surround_vote).
|
||||||
|
|
||||||
Specifically, when signing an `Attestation`, a validator should perform the following steps in the following order:
|
Specifically, when signing an `Attestation`, a validator should perform the following steps in the following order:
|
||||||
1. Save a record to hard disk that an attestation has been signed for source -- `attestation_data.justified_epoch` -- and target -- `slot_to_epoch(attestation_data.slot)`.
|
1. Save a record to hard disk that an attestation has been signed for source -- `attestation_data.source_epoch` -- and target -- `slot_to_epoch(attestation_data.slot)`.
|
||||||
2. Generate and broadcast attestation.
|
2. Generate and broadcast attestation.
|
||||||
|
|
||||||
If the software crashes at some point within this routine, then when the validator comes back online the hard disk has the record of the _potentially_ signed/broadcast attestation and can effectively avoid slashing.
|
If the software crashes at some point within this routine, then when the validator comes back online the hard disk has the record of the _potentially_ signed/broadcast attestation and can effectively avoid slashing.
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import build.phase0.spec as spec
|
||||||
|
|
||||||
|
from build.phase0.state_transition import (
|
||||||
|
state_transition,
|
||||||
|
)
|
||||||
|
from build.phase0.spec import (
|
||||||
|
ZERO_HASH,
|
||||||
|
get_current_epoch,
|
||||||
|
process_attestation,
|
||||||
|
slot_to_epoch,
|
||||||
|
)
|
||||||
|
from tests.phase0.helpers import (
|
||||||
|
build_empty_block_for_next_slot,
|
||||||
|
get_valid_attestation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# mark entire file as 'attestations'
|
||||||
|
pytestmark = pytest.mark.attestations
|
||||||
|
|
||||||
|
|
||||||
|
def run_attestation_processing(state, attestation, valid=True):
|
||||||
|
"""
|
||||||
|
Run ``process_attestation`` returning the pre and post state.
|
||||||
|
If ``valid == False``, run expecting ``AssertionError``
|
||||||
|
"""
|
||||||
|
post_state = deepcopy(state)
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_attestation(post_state, attestation)
|
||||||
|
return state, None
|
||||||
|
|
||||||
|
process_attestation(post_state, attestation)
|
||||||
|
|
||||||
|
current_epoch = get_current_epoch(state)
|
||||||
|
target_epoch = slot_to_epoch(attestation.data.slot)
|
||||||
|
if target_epoch == current_epoch:
|
||||||
|
assert len(post_state.current_epoch_attestations) == len(state.current_epoch_attestations) + 1
|
||||||
|
else:
|
||||||
|
assert len(post_state.previous_epoch_attestations) == len(state.previous_epoch_attestations) + 1
|
||||||
|
|
||||||
|
return state, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_success(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_success_prevous_epoch(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
block = build_empty_block_for_next_slot(state)
|
||||||
|
block.slot = state.slot + spec.SLOTS_PER_EPOCH
|
||||||
|
state_transition(state, block)
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_before_inclusion_delay(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
# do not increment slot to allow for inclusion delay
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_after_epoch_slots(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
block = build_empty_block_for_next_slot(state)
|
||||||
|
# increment past latest inclusion slot
|
||||||
|
block.slot = state.slot + spec.SLOTS_PER_EPOCH + 1
|
||||||
|
state_transition(state, block)
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_source_epoch(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
|
||||||
|
attestation.data.source_epoch += 10
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_source_root(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
|
||||||
|
attestation.data.source_root = b'\x42'*32
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_zero_crosslink_data_root(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
|
||||||
|
attestation.data.crosslink_data_root = b'\x42'*32
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_previous_crosslink(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
|
||||||
|
state.latest_crosslinks[attestation.data.shard].epoch += 10
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_empty_custody_bitfield(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
|
||||||
|
attestation.custody_bitfield = deepcopy(attestation.aggregation_bitfield)
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_aggregation_bitfield(state):
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
|
||||||
|
attestation.aggregation_bitfield = b'\x00' * len(attestation.aggregation_bitfield)
|
||||||
|
|
||||||
|
pre_state, post_state = run_attestation_processing(state, attestation, False)
|
||||||
|
|
||||||
|
return pre_state, attestation, post_state
|
|
@ -0,0 +1,71 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
from build.phase0.spec import (
|
||||||
|
get_beacon_proposer_index,
|
||||||
|
cache_state,
|
||||||
|
advance_slot,
|
||||||
|
process_block_header,
|
||||||
|
)
|
||||||
|
from tests.phase0.helpers import (
|
||||||
|
build_empty_block_for_next_slot,
|
||||||
|
)
|
||||||
|
|
||||||
|
# mark entire file as 'header'
|
||||||
|
pytestmark = pytest.mark.header
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_state_for_header_processing(state):
|
||||||
|
cache_state(state)
|
||||||
|
advance_slot(state)
|
||||||
|
|
||||||
|
|
||||||
|
def run_block_header_processing(state, block, valid=True):
|
||||||
|
"""
|
||||||
|
Run ``process_block_header`` returning the pre and post state.
|
||||||
|
If ``valid == False``, run expecting ``AssertionError``
|
||||||
|
"""
|
||||||
|
prepare_state_for_header_processing(state)
|
||||||
|
post_state = deepcopy(state)
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_block_header(post_state, block)
|
||||||
|
return state, None
|
||||||
|
|
||||||
|
process_block_header(post_state, block)
|
||||||
|
return state, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_success(state):
|
||||||
|
block = build_empty_block_for_next_slot(state)
|
||||||
|
pre_state, post_state = run_block_header_processing(state, block)
|
||||||
|
return state, block, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_slot(state):
|
||||||
|
block = build_empty_block_for_next_slot(state)
|
||||||
|
block.slot = state.slot + 2 # invalid slot
|
||||||
|
|
||||||
|
pre_state, post_state = run_block_header_processing(state, block, valid=False)
|
||||||
|
return pre_state, block, None
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_previous_block_root(state):
|
||||||
|
block = build_empty_block_for_next_slot(state)
|
||||||
|
block.previous_block_root = b'\12'*32 # invalid prev root
|
||||||
|
|
||||||
|
pre_state, post_state = run_block_header_processing(state, block, valid=False)
|
||||||
|
return pre_state, block, None
|
||||||
|
|
||||||
|
|
||||||
|
def test_proposer_slashed(state):
|
||||||
|
# set proposer to slashed
|
||||||
|
proposer_index = get_beacon_proposer_index(state, state.slot + 1)
|
||||||
|
state.validator_registry[proposer_index].slashed = True
|
||||||
|
|
||||||
|
block = build_empty_block_for_next_slot(state)
|
||||||
|
|
||||||
|
pre_state, post_state = run_block_header_processing(state, block, valid=False)
|
||||||
|
return pre_state, block, None
|
|
@ -0,0 +1,141 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import build.phase0.spec as spec
|
||||||
|
|
||||||
|
from build.phase0.spec import (
|
||||||
|
get_balance,
|
||||||
|
ZERO_HASH,
|
||||||
|
process_deposit,
|
||||||
|
)
|
||||||
|
from tests.phase0.helpers import (
|
||||||
|
build_deposit,
|
||||||
|
privkeys,
|
||||||
|
pubkeys,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# mark entire file as 'voluntary_exits'
|
||||||
|
pytestmark = pytest.mark.voluntary_exits
|
||||||
|
|
||||||
|
|
||||||
|
def test_success(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
# fill previous deposits with zero-hash
|
||||||
|
deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry)
|
||||||
|
|
||||||
|
index = len(deposit_data_leaves)
|
||||||
|
pubkey = pubkeys[index]
|
||||||
|
privkey = privkeys[index]
|
||||||
|
deposit, root, deposit_data_leaves = build_deposit(
|
||||||
|
pre_state,
|
||||||
|
deposit_data_leaves,
|
||||||
|
pubkey,
|
||||||
|
privkey,
|
||||||
|
spec.MAX_DEPOSIT_AMOUNT,
|
||||||
|
)
|
||||||
|
|
||||||
|
pre_state.latest_eth1_data.deposit_root = root
|
||||||
|
pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves)
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
process_deposit(post_state, deposit)
|
||||||
|
|
||||||
|
assert len(post_state.validator_registry) == len(state.validator_registry) + 1
|
||||||
|
assert len(post_state.balances) == len(state.balances) + 1
|
||||||
|
assert post_state.validator_registry[index].pubkey == pubkeys[index]
|
||||||
|
assert get_balance(post_state, index) == spec.MAX_DEPOSIT_AMOUNT
|
||||||
|
assert post_state.deposit_index == post_state.latest_eth1_data.deposit_count
|
||||||
|
|
||||||
|
return pre_state, deposit, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_success_top_up(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry)
|
||||||
|
|
||||||
|
validator_index = 0
|
||||||
|
amount = spec.MAX_DEPOSIT_AMOUNT // 4
|
||||||
|
pubkey = pubkeys[validator_index]
|
||||||
|
privkey = privkeys[validator_index]
|
||||||
|
deposit, root, deposit_data_leaves = build_deposit(
|
||||||
|
pre_state,
|
||||||
|
deposit_data_leaves,
|
||||||
|
pubkey,
|
||||||
|
privkey,
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
pre_state.latest_eth1_data.deposit_root = root
|
||||||
|
pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves)
|
||||||
|
pre_balance = get_balance(pre_state, validator_index)
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
process_deposit(post_state, deposit)
|
||||||
|
|
||||||
|
assert len(post_state.validator_registry) == len(state.validator_registry)
|
||||||
|
assert len(post_state.balances) == len(state.balances)
|
||||||
|
assert post_state.deposit_index == post_state.latest_eth1_data.deposit_count
|
||||||
|
assert get_balance(post_state, validator_index) == pre_balance + amount
|
||||||
|
|
||||||
|
return pre_state, deposit, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrong_index(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry)
|
||||||
|
|
||||||
|
index = len(deposit_data_leaves)
|
||||||
|
pubkey = pubkeys[index]
|
||||||
|
privkey = privkeys[index]
|
||||||
|
deposit, root, deposit_data_leaves = build_deposit(
|
||||||
|
pre_state,
|
||||||
|
deposit_data_leaves,
|
||||||
|
pubkey,
|
||||||
|
privkey,
|
||||||
|
spec.MAX_DEPOSIT_AMOUNT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# mess up deposit_index
|
||||||
|
deposit.index = pre_state.deposit_index + 1
|
||||||
|
|
||||||
|
pre_state.latest_eth1_data.deposit_root = root
|
||||||
|
pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves)
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_deposit(post_state, deposit)
|
||||||
|
|
||||||
|
return pre_state, deposit, None
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_merkle_proof(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry)
|
||||||
|
|
||||||
|
index = len(deposit_data_leaves)
|
||||||
|
pubkey = pubkeys[index]
|
||||||
|
privkey = privkeys[index]
|
||||||
|
deposit, root, deposit_data_leaves = build_deposit(
|
||||||
|
pre_state,
|
||||||
|
deposit_data_leaves,
|
||||||
|
pubkey,
|
||||||
|
privkey,
|
||||||
|
spec.MAX_DEPOSIT_AMOUNT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# mess up merkle branch
|
||||||
|
deposit.proof[-1] = spec.ZERO_HASH
|
||||||
|
|
||||||
|
pre_state.latest_eth1_data.deposit_root = root
|
||||||
|
pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves)
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_deposit(post_state, deposit)
|
||||||
|
|
||||||
|
return pre_state, deposit, None
|
|
@ -0,0 +1,97 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import build.phase0.spec as spec
|
||||||
|
from build.phase0.spec import (
|
||||||
|
get_balance,
|
||||||
|
get_current_epoch,
|
||||||
|
process_proposer_slashing,
|
||||||
|
)
|
||||||
|
from tests.phase0.helpers import (
|
||||||
|
get_valid_proposer_slashing,
|
||||||
|
)
|
||||||
|
|
||||||
|
# mark entire file as 'header'
|
||||||
|
pytestmark = pytest.mark.proposer_slashings
|
||||||
|
|
||||||
|
|
||||||
|
def run_proposer_slashing_processing(state, proposer_slashing, valid=True):
|
||||||
|
"""
|
||||||
|
Run ``process_proposer_slashing`` returning the pre and post state.
|
||||||
|
If ``valid == False``, run expecting ``AssertionError``
|
||||||
|
"""
|
||||||
|
post_state = deepcopy(state)
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_proposer_slashing(post_state, proposer_slashing)
|
||||||
|
return state, None
|
||||||
|
|
||||||
|
process_proposer_slashing(post_state, proposer_slashing)
|
||||||
|
|
||||||
|
slashed_validator = post_state.validator_registry[proposer_slashing.proposer_index]
|
||||||
|
assert not slashed_validator.initiated_exit
|
||||||
|
assert slashed_validator.slashed
|
||||||
|
assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
|
||||||
|
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
|
||||||
|
# lost whistleblower reward
|
||||||
|
assert (
|
||||||
|
get_balance(post_state, proposer_slashing.proposer_index) <
|
||||||
|
get_balance(state, proposer_slashing.proposer_index)
|
||||||
|
)
|
||||||
|
|
||||||
|
return state, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_success(state):
|
||||||
|
proposer_slashing = get_valid_proposer_slashing(state)
|
||||||
|
|
||||||
|
pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing)
|
||||||
|
|
||||||
|
return pre_state, proposer_slashing, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_epochs_are_different(state):
|
||||||
|
proposer_slashing = get_valid_proposer_slashing(state)
|
||||||
|
|
||||||
|
# set slots to be in different epochs
|
||||||
|
proposer_slashing.header_2.slot += spec.SLOTS_PER_EPOCH
|
||||||
|
|
||||||
|
pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False)
|
||||||
|
|
||||||
|
return pre_state, proposer_slashing, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_headers_are_same(state):
|
||||||
|
proposer_slashing = get_valid_proposer_slashing(state)
|
||||||
|
|
||||||
|
# set headers to be the same
|
||||||
|
proposer_slashing.header_2 = proposer_slashing.header_1
|
||||||
|
|
||||||
|
pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False)
|
||||||
|
|
||||||
|
return pre_state, proposer_slashing, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_proposer_is_slashed(state):
|
||||||
|
proposer_slashing = get_valid_proposer_slashing(state)
|
||||||
|
|
||||||
|
# set proposer to slashed
|
||||||
|
state.validator_registry[proposer_slashing.proposer_index].slashed = True
|
||||||
|
|
||||||
|
pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False)
|
||||||
|
|
||||||
|
return pre_state, proposer_slashing, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_proposer_is_withdrawn(state):
|
||||||
|
proposer_slashing = get_valid_proposer_slashing(state)
|
||||||
|
|
||||||
|
# set proposer withdrawable_epoch in past
|
||||||
|
current_epoch = get_current_epoch(state)
|
||||||
|
proposer_index = proposer_slashing.proposer_index
|
||||||
|
state.validator_registry[proposer_index].withdrawable_epoch = current_epoch - 1
|
||||||
|
|
||||||
|
pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False)
|
||||||
|
|
||||||
|
return pre_state, proposer_slashing, post_state
|
|
@ -0,0 +1,175 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import build.phase0.spec as spec
|
||||||
|
|
||||||
|
from build.phase0.spec import (
|
||||||
|
get_active_validator_indices,
|
||||||
|
get_current_epoch,
|
||||||
|
process_voluntary_exit,
|
||||||
|
)
|
||||||
|
from tests.phase0.helpers import (
|
||||||
|
build_voluntary_exit,
|
||||||
|
pubkey_to_privkey,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# mark entire file as 'voluntary_exits'
|
||||||
|
pytestmark = pytest.mark.voluntary_exits
|
||||||
|
|
||||||
|
|
||||||
|
def test_success(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
#
|
||||||
|
# setup pre_state
|
||||||
|
#
|
||||||
|
# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
|
||||||
|
pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
|
||||||
|
|
||||||
|
#
|
||||||
|
# build voluntary exit
|
||||||
|
#
|
||||||
|
current_epoch = get_current_epoch(pre_state)
|
||||||
|
validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0]
|
||||||
|
privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey]
|
||||||
|
|
||||||
|
voluntary_exit = build_voluntary_exit(
|
||||||
|
pre_state,
|
||||||
|
current_epoch,
|
||||||
|
validator_index,
|
||||||
|
privkey,
|
||||||
|
)
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
#
|
||||||
|
# test valid exit
|
||||||
|
#
|
||||||
|
process_voluntary_exit(post_state, voluntary_exit)
|
||||||
|
|
||||||
|
assert not pre_state.validator_registry[validator_index].initiated_exit
|
||||||
|
assert post_state.validator_registry[validator_index].initiated_exit
|
||||||
|
|
||||||
|
return pre_state, voluntary_exit, post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_validator_not_active(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
current_epoch = get_current_epoch(pre_state)
|
||||||
|
validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0]
|
||||||
|
privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey]
|
||||||
|
|
||||||
|
#
|
||||||
|
# setup pre_state
|
||||||
|
#
|
||||||
|
pre_state.validator_registry[validator_index].activation_epoch = spec.FAR_FUTURE_EPOCH
|
||||||
|
|
||||||
|
#
|
||||||
|
# build and test voluntary exit
|
||||||
|
#
|
||||||
|
voluntary_exit = build_voluntary_exit(
|
||||||
|
pre_state,
|
||||||
|
current_epoch,
|
||||||
|
validator_index,
|
||||||
|
privkey,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_voluntary_exit(pre_state, voluntary_exit)
|
||||||
|
|
||||||
|
return pre_state, voluntary_exit, None
|
||||||
|
|
||||||
|
|
||||||
|
def test_validator_already_exited(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
#
|
||||||
|
# setup pre_state
|
||||||
|
#
|
||||||
|
# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit
|
||||||
|
pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
|
||||||
|
|
||||||
|
current_epoch = get_current_epoch(pre_state)
|
||||||
|
validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0]
|
||||||
|
privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey]
|
||||||
|
|
||||||
|
# but validator already has exited
|
||||||
|
pre_state.validator_registry[validator_index].exit_epoch = current_epoch + 2
|
||||||
|
|
||||||
|
#
|
||||||
|
# build voluntary exit
|
||||||
|
#
|
||||||
|
voluntary_exit = build_voluntary_exit(
|
||||||
|
pre_state,
|
||||||
|
current_epoch,
|
||||||
|
validator_index,
|
||||||
|
privkey,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_voluntary_exit(pre_state, voluntary_exit)
|
||||||
|
|
||||||
|
return pre_state, voluntary_exit, None
|
||||||
|
|
||||||
|
|
||||||
|
def test_validator_already_initiated_exit(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
#
|
||||||
|
# setup pre_state
|
||||||
|
#
|
||||||
|
# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit
|
||||||
|
pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
|
||||||
|
|
||||||
|
current_epoch = get_current_epoch(pre_state)
|
||||||
|
validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0]
|
||||||
|
privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey]
|
||||||
|
|
||||||
|
# but validator already has initiated exit
|
||||||
|
pre_state.validator_registry[validator_index].initiated_exit = True
|
||||||
|
|
||||||
|
#
|
||||||
|
# build voluntary exit
|
||||||
|
#
|
||||||
|
voluntary_exit = build_voluntary_exit(
|
||||||
|
pre_state,
|
||||||
|
current_epoch,
|
||||||
|
validator_index,
|
||||||
|
privkey,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_voluntary_exit(pre_state, voluntary_exit)
|
||||||
|
|
||||||
|
return pre_state, voluntary_exit, None
|
||||||
|
|
||||||
|
|
||||||
|
def test_validator_not_active_long_enough(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
#
|
||||||
|
# setup pre_state
|
||||||
|
#
|
||||||
|
current_epoch = get_current_epoch(pre_state)
|
||||||
|
validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0]
|
||||||
|
privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey]
|
||||||
|
|
||||||
|
# but validator already has initiated exit
|
||||||
|
pre_state.validator_registry[validator_index].initiated_exit = True
|
||||||
|
|
||||||
|
#
|
||||||
|
# build voluntary exit
|
||||||
|
#
|
||||||
|
voluntary_exit = build_voluntary_exit(
|
||||||
|
pre_state,
|
||||||
|
current_epoch,
|
||||||
|
validator_index,
|
||||||
|
privkey,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
current_epoch - pre_state.validator_registry[validator_index].activation_epoch <
|
||||||
|
spec.PERSISTENT_COMMITTEE_PERIOD
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
process_voluntary_exit(pre_state, voluntary_exit)
|
||||||
|
|
||||||
|
return pre_state, voluntary_exit, None
|
|
@ -0,0 +1,70 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from build.phase0 import spec
|
||||||
|
|
||||||
|
from tests.phase0.helpers import (
|
||||||
|
create_genesis_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {} # no change
|
||||||
|
|
||||||
|
MINIMAL_CONFIG = {
|
||||||
|
"SHARD_COUNT": 8,
|
||||||
|
"MIN_ATTESTATION_INCLUSION_DELAY": 2,
|
||||||
|
"TARGET_COMMITTEE_SIZE": 4,
|
||||||
|
"SLOTS_PER_EPOCH": 8,
|
||||||
|
"GENESIS_EPOCH": spec.GENESIS_SLOT // 8,
|
||||||
|
"SLOTS_PER_HISTORICAL_ROOT": 64,
|
||||||
|
"LATEST_RANDAO_MIXES_LENGTH": 64,
|
||||||
|
"LATEST_ACTIVE_INDEX_ROOTS_LENGTH": 64,
|
||||||
|
"LATEST_SLASHED_EXIT_LENGTH": 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def overwrite_spec_config(config):
|
||||||
|
for field in config:
|
||||||
|
setattr(spec, field, config[field])
|
||||||
|
if field == "LATEST_RANDAO_MIXES_LENGTH":
|
||||||
|
spec.BeaconState.fields['latest_randao_mixes'][1] = config[field]
|
||||||
|
elif field == "SHARD_COUNT":
|
||||||
|
spec.BeaconState.fields['latest_crosslinks'][1] = config[field]
|
||||||
|
elif field == "SLOTS_PER_HISTORICAL_ROOT":
|
||||||
|
spec.BeaconState.fields['latest_block_roots'][1] = config[field]
|
||||||
|
spec.BeaconState.fields['latest_state_roots'][1] = config[field]
|
||||||
|
spec.HistoricalBatch.fields['block_roots'][1] = config[field]
|
||||||
|
spec.HistoricalBatch.fields['state_roots'][1] = config[field]
|
||||||
|
elif field == "LATEST_ACTIVE_INDEX_ROOTS_LENGTH":
|
||||||
|
spec.BeaconState.fields['latest_active_index_roots'][1] = config[field]
|
||||||
|
elif field == "LATEST_SLASHED_EXIT_LENGTH":
|
||||||
|
spec.BeaconState.fields['latest_slashed_balances'][1] = config[field]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(
|
||||||
|
params=[
|
||||||
|
pytest.param(MINIMAL_CONFIG, marks=pytest.mark.minimal_config),
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def config(request):
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def overwrite_config(config):
|
||||||
|
overwrite_spec_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def num_validators():
|
||||||
|
return 100
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def deposit_data_leaves():
|
||||||
|
return list()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def state(num_validators, deposit_data_leaves):
|
||||||
|
return create_genesis_state(num_validators, deposit_data_leaves)
|
|
@ -0,0 +1,301 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from py_ecc import bls
|
||||||
|
|
||||||
|
import build.phase0.spec as spec
|
||||||
|
from build.phase0.utils.minimal_ssz import signed_root
|
||||||
|
from build.phase0.spec import (
|
||||||
|
# constants
|
||||||
|
EMPTY_SIGNATURE,
|
||||||
|
ZERO_HASH,
|
||||||
|
# SSZ
|
||||||
|
Attestation,
|
||||||
|
AttestationData,
|
||||||
|
AttestationDataAndCustodyBit,
|
||||||
|
BeaconBlockHeader,
|
||||||
|
Deposit,
|
||||||
|
DepositData,
|
||||||
|
Eth1Data,
|
||||||
|
ProposerSlashing,
|
||||||
|
VoluntaryExit,
|
||||||
|
# functions
|
||||||
|
get_active_validator_indices,
|
||||||
|
get_attestation_participants,
|
||||||
|
get_block_root,
|
||||||
|
get_crosslink_committee_for_attestation,
|
||||||
|
get_crosslink_committees_at_slot,
|
||||||
|
get_current_epoch,
|
||||||
|
get_domain,
|
||||||
|
get_empty_block,
|
||||||
|
get_epoch_start_slot,
|
||||||
|
get_genesis_beacon_state,
|
||||||
|
slot_to_epoch,
|
||||||
|
verify_merkle_branch,
|
||||||
|
hash,
|
||||||
|
)
|
||||||
|
from build.phase0.utils.merkle_minimal import (
|
||||||
|
calc_merkle_tree_from_leaves,
|
||||||
|
get_merkle_proof,
|
||||||
|
get_merkle_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
privkeys = [i + 1 for i in range(1000)]
|
||||||
|
pubkeys = [bls.privtopub(privkey) for privkey in privkeys]
|
||||||
|
pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)}
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_genesis_validator_deposits(num_validators, deposit_data_leaves=None):
|
||||||
|
if not deposit_data_leaves:
|
||||||
|
deposit_data_leaves = []
|
||||||
|
proof_of_possession = b'\x33' * 96
|
||||||
|
|
||||||
|
deposit_data_list = []
|
||||||
|
for i in range(num_validators):
|
||||||
|
pubkey = pubkeys[i]
|
||||||
|
deposit_data = DepositData(
|
||||||
|
pubkey=pubkey,
|
||||||
|
# insecurely use pubkey as withdrawal key as well
|
||||||
|
withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(pubkey)[1:],
|
||||||
|
amount=spec.MAX_DEPOSIT_AMOUNT,
|
||||||
|
proof_of_possession=proof_of_possession,
|
||||||
|
)
|
||||||
|
item = hash(deposit_data.serialize())
|
||||||
|
deposit_data_leaves.append(item)
|
||||||
|
tree = calc_merkle_tree_from_leaves(tuple(deposit_data_leaves))
|
||||||
|
root = get_merkle_root((tuple(deposit_data_leaves)))
|
||||||
|
proof = list(get_merkle_proof(tree, item_index=i))
|
||||||
|
assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, i, root)
|
||||||
|
deposit_data_list.append(deposit_data)
|
||||||
|
|
||||||
|
genesis_validator_deposits = []
|
||||||
|
for i in range(num_validators):
|
||||||
|
genesis_validator_deposits.append(Deposit(
|
||||||
|
proof=list(get_merkle_proof(tree, item_index=i)),
|
||||||
|
index=i,
|
||||||
|
data=deposit_data_list[i]
|
||||||
|
))
|
||||||
|
return genesis_validator_deposits, root
|
||||||
|
|
||||||
|
|
||||||
|
def create_genesis_state(num_validators, deposit_data_leaves=None):
|
||||||
|
initial_deposits, deposit_root = create_mock_genesis_validator_deposits(
|
||||||
|
num_validators,
|
||||||
|
deposit_data_leaves,
|
||||||
|
)
|
||||||
|
return get_genesis_beacon_state(
|
||||||
|
initial_deposits,
|
||||||
|
genesis_time=0,
|
||||||
|
genesis_eth1_data=Eth1Data(
|
||||||
|
deposit_root=deposit_root,
|
||||||
|
deposit_count=len(initial_deposits),
|
||||||
|
block_hash=spec.ZERO_HASH,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def force_registry_change_at_next_epoch(state):
|
||||||
|
# artificially trigger registry update at next epoch transition
|
||||||
|
state.finalized_epoch = get_current_epoch(state) - 1
|
||||||
|
for crosslink in state.latest_crosslinks:
|
||||||
|
crosslink.epoch = state.finalized_epoch
|
||||||
|
state.validator_registry_update_epoch = state.finalized_epoch - 1
|
||||||
|
|
||||||
|
|
||||||
|
def build_empty_block_for_next_slot(state):
|
||||||
|
empty_block = get_empty_block()
|
||||||
|
empty_block.slot = state.slot + 1
|
||||||
|
previous_block_header = deepcopy(state.latest_block_header)
|
||||||
|
if previous_block_header.state_root == spec.ZERO_HASH:
|
||||||
|
previous_block_header.state_root = state.hash_tree_root()
|
||||||
|
empty_block.previous_block_root = signed_root(previous_block_header)
|
||||||
|
return empty_block
|
||||||
|
|
||||||
|
|
||||||
|
def build_deposit_data(state, pubkey, privkey, amount):
|
||||||
|
deposit_data = DepositData(
|
||||||
|
pubkey=pubkey,
|
||||||
|
# insecurely use pubkey as withdrawal key as well
|
||||||
|
withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(pubkey)[1:],
|
||||||
|
amount=amount,
|
||||||
|
proof_of_possession=EMPTY_SIGNATURE,
|
||||||
|
)
|
||||||
|
proof_of_possession = bls.sign(
|
||||||
|
message_hash=signed_root(deposit_data),
|
||||||
|
privkey=privkey,
|
||||||
|
domain=get_domain(
|
||||||
|
state.fork,
|
||||||
|
get_current_epoch(state),
|
||||||
|
spec.DOMAIN_DEPOSIT,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
deposit_data.proof_of_possession = proof_of_possession
|
||||||
|
return deposit_data
|
||||||
|
|
||||||
|
|
||||||
|
def build_attestation_data(state, slot, shard):
|
||||||
|
assert state.slot >= slot
|
||||||
|
|
||||||
|
block_root = build_empty_block_for_next_slot(state).previous_block_root
|
||||||
|
|
||||||
|
epoch_start_slot = get_epoch_start_slot(get_current_epoch(state))
|
||||||
|
if epoch_start_slot == slot:
|
||||||
|
epoch_boundary_root = block_root
|
||||||
|
else:
|
||||||
|
get_block_root(state, epoch_start_slot)
|
||||||
|
|
||||||
|
if slot < epoch_start_slot:
|
||||||
|
justified_block_root = state.previous_justified_root
|
||||||
|
else:
|
||||||
|
justified_block_root = state.current_justified_root
|
||||||
|
|
||||||
|
return AttestationData(
|
||||||
|
slot=slot,
|
||||||
|
shard=shard,
|
||||||
|
beacon_block_root=block_root,
|
||||||
|
source_epoch=state.current_justified_epoch,
|
||||||
|
source_root=justified_block_root,
|
||||||
|
target_root=epoch_boundary_root,
|
||||||
|
crosslink_data_root=spec.ZERO_HASH,
|
||||||
|
previous_crosslink=deepcopy(state.latest_crosslinks[shard]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_voluntary_exit(state, epoch, validator_index, privkey):
|
||||||
|
voluntary_exit = VoluntaryExit(
|
||||||
|
epoch=epoch,
|
||||||
|
validator_index=validator_index,
|
||||||
|
signature=EMPTY_SIGNATURE,
|
||||||
|
)
|
||||||
|
voluntary_exit.signature = bls.sign(
|
||||||
|
message_hash=signed_root(voluntary_exit),
|
||||||
|
privkey=privkey,
|
||||||
|
domain=get_domain(
|
||||||
|
fork=state.fork,
|
||||||
|
epoch=epoch,
|
||||||
|
domain_type=spec.DOMAIN_VOLUNTARY_EXIT,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return voluntary_exit
|
||||||
|
|
||||||
|
|
||||||
|
def build_deposit(state,
|
||||||
|
deposit_data_leaves,
|
||||||
|
pubkey,
|
||||||
|
privkey,
|
||||||
|
amount):
|
||||||
|
deposit_data = build_deposit_data(state, pubkey, privkey, amount)
|
||||||
|
|
||||||
|
item = hash(deposit_data.serialize())
|
||||||
|
index = len(deposit_data_leaves)
|
||||||
|
deposit_data_leaves.append(item)
|
||||||
|
tree = calc_merkle_tree_from_leaves(tuple(deposit_data_leaves))
|
||||||
|
root = get_merkle_root((tuple(deposit_data_leaves)))
|
||||||
|
proof = list(get_merkle_proof(tree, item_index=index))
|
||||||
|
assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, index, root)
|
||||||
|
|
||||||
|
deposit = Deposit(
|
||||||
|
proof=list(proof),
|
||||||
|
index=index,
|
||||||
|
data=deposit_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
return deposit, root, deposit_data_leaves
|
||||||
|
|
||||||
|
|
||||||
|
def get_valid_proposer_slashing(state):
|
||||||
|
current_epoch = get_current_epoch(state)
|
||||||
|
validator_index = get_active_validator_indices(state.validator_registry, current_epoch)[-1]
|
||||||
|
privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey]
|
||||||
|
slot = state.slot
|
||||||
|
|
||||||
|
header_1 = BeaconBlockHeader(
|
||||||
|
slot=slot,
|
||||||
|
previous_block_root=ZERO_HASH,
|
||||||
|
state_root=ZERO_HASH,
|
||||||
|
block_body_root=ZERO_HASH,
|
||||||
|
signature=EMPTY_SIGNATURE,
|
||||||
|
)
|
||||||
|
header_2 = deepcopy(header_1)
|
||||||
|
header_2.previous_block_root = b'\x02' * 32
|
||||||
|
header_2.slot = slot + 1
|
||||||
|
|
||||||
|
domain = get_domain(
|
||||||
|
fork=state.fork,
|
||||||
|
epoch=get_current_epoch(state),
|
||||||
|
domain_type=spec.DOMAIN_BEACON_BLOCK,
|
||||||
|
)
|
||||||
|
header_1.signature = bls.sign(
|
||||||
|
message_hash=signed_root(header_1),
|
||||||
|
privkey=privkey,
|
||||||
|
domain=domain,
|
||||||
|
)
|
||||||
|
header_2.signature = bls.sign(
|
||||||
|
message_hash=signed_root(header_2),
|
||||||
|
privkey=privkey,
|
||||||
|
domain=domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ProposerSlashing(
|
||||||
|
proposer_index=validator_index,
|
||||||
|
header_1=header_1,
|
||||||
|
header_2=header_2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_valid_attestation(state, slot=None):
|
||||||
|
if slot is None:
|
||||||
|
slot = state.slot
|
||||||
|
shard = state.latest_start_shard
|
||||||
|
attestation_data = build_attestation_data(state, slot, shard)
|
||||||
|
|
||||||
|
crosslink_committee = get_crosslink_committee_for_attestation(state, attestation_data)
|
||||||
|
|
||||||
|
committee_size = len(crosslink_committee)
|
||||||
|
bitfield_length = (committee_size + 7) // 8
|
||||||
|
aggregation_bitfield = b'\xC0' + b'\x00' * (bitfield_length - 1)
|
||||||
|
custody_bitfield = b'\x00' * bitfield_length
|
||||||
|
attestation = Attestation(
|
||||||
|
aggregation_bitfield=aggregation_bitfield,
|
||||||
|
data=attestation_data,
|
||||||
|
custody_bitfield=custody_bitfield,
|
||||||
|
aggregate_signature=EMPTY_SIGNATURE,
|
||||||
|
)
|
||||||
|
participants = get_attestation_participants(
|
||||||
|
state,
|
||||||
|
attestation.data,
|
||||||
|
attestation.aggregation_bitfield,
|
||||||
|
)
|
||||||
|
assert len(participants) == 2
|
||||||
|
|
||||||
|
signatures = []
|
||||||
|
for validator_index in participants:
|
||||||
|
privkey = privkeys[validator_index]
|
||||||
|
signatures.append(
|
||||||
|
get_attestation_signature(
|
||||||
|
state,
|
||||||
|
attestation.data,
|
||||||
|
privkey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
attestation.aggregation_signature = bls.aggregate_signatures(signatures)
|
||||||
|
return attestation
|
||||||
|
|
||||||
|
|
||||||
|
def get_attestation_signature(state, attestation_data, privkey, custody_bit=0b0):
|
||||||
|
message_hash = AttestationDataAndCustodyBit(
|
||||||
|
data=attestation_data,
|
||||||
|
custody_bit=custody_bit,
|
||||||
|
).hash_tree_root()
|
||||||
|
|
||||||
|
return bls.sign(
|
||||||
|
message_hash=message_hash,
|
||||||
|
privkey=privkey,
|
||||||
|
domain=get_domain(
|
||||||
|
fork=state.fork,
|
||||||
|
epoch=slot_to_epoch(attestation_data.slot),
|
||||||
|
domain_type=spec.DOMAIN_ATTESTATION,
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,425 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from py_ecc import bls
|
||||||
|
import build.phase0.spec as spec
|
||||||
|
|
||||||
|
from build.phase0.utils.minimal_ssz import signed_root
|
||||||
|
from build.phase0.spec import (
|
||||||
|
# constants
|
||||||
|
EMPTY_SIGNATURE,
|
||||||
|
ZERO_HASH,
|
||||||
|
# SSZ
|
||||||
|
Deposit,
|
||||||
|
Transfer,
|
||||||
|
VoluntaryExit,
|
||||||
|
# functions
|
||||||
|
get_active_validator_indices,
|
||||||
|
get_balance,
|
||||||
|
get_block_root,
|
||||||
|
get_current_epoch,
|
||||||
|
get_domain,
|
||||||
|
get_state_root,
|
||||||
|
advance_slot,
|
||||||
|
cache_state,
|
||||||
|
set_balance,
|
||||||
|
verify_merkle_branch,
|
||||||
|
hash,
|
||||||
|
)
|
||||||
|
from build.phase0.state_transition import (
|
||||||
|
state_transition,
|
||||||
|
)
|
||||||
|
from build.phase0.utils.merkle_minimal import (
|
||||||
|
calc_merkle_tree_from_leaves,
|
||||||
|
get_merkle_proof,
|
||||||
|
get_merkle_root,
|
||||||
|
)
|
||||||
|
from tests.phase0.helpers import (
|
||||||
|
build_deposit_data,
|
||||||
|
build_empty_block_for_next_slot,
|
||||||
|
force_registry_change_at_next_epoch,
|
||||||
|
get_valid_attestation,
|
||||||
|
get_valid_proposer_slashing,
|
||||||
|
privkeys,
|
||||||
|
pubkeys,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# mark entire file as 'sanity'
|
||||||
|
pytestmark = pytest.mark.sanity
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_transition(state):
|
||||||
|
test_state = deepcopy(state)
|
||||||
|
cache_state(test_state)
|
||||||
|
advance_slot(test_state)
|
||||||
|
assert test_state.slot == state.slot + 1
|
||||||
|
assert get_state_root(test_state, state.slot) == state.hash_tree_root()
|
||||||
|
return test_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_block_transition(state):
|
||||||
|
test_state = deepcopy(state)
|
||||||
|
|
||||||
|
block = build_empty_block_for_next_slot(test_state)
|
||||||
|
state_transition(test_state, block)
|
||||||
|
|
||||||
|
assert len(test_state.eth1_data_votes) == len(state.eth1_data_votes) + 1
|
||||||
|
assert get_block_root(test_state, state.slot) == block.previous_block_root
|
||||||
|
|
||||||
|
return state, [block], test_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_skipped_slots(state):
|
||||||
|
test_state = deepcopy(state)
|
||||||
|
block = build_empty_block_for_next_slot(test_state)
|
||||||
|
block.slot += 3
|
||||||
|
|
||||||
|
state_transition(test_state, block)
|
||||||
|
|
||||||
|
assert test_state.slot == block.slot
|
||||||
|
for slot in range(state.slot, test_state.slot):
|
||||||
|
assert get_block_root(test_state, slot) == block.previous_block_root
|
||||||
|
|
||||||
|
return state, [block], test_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_epoch_transition(state):
|
||||||
|
test_state = deepcopy(state)
|
||||||
|
block = build_empty_block_for_next_slot(test_state)
|
||||||
|
block.slot += spec.SLOTS_PER_EPOCH
|
||||||
|
|
||||||
|
state_transition(test_state, block)
|
||||||
|
|
||||||
|
assert test_state.slot == block.slot
|
||||||
|
for slot in range(state.slot, test_state.slot):
|
||||||
|
assert get_block_root(test_state, slot) == block.previous_block_root
|
||||||
|
|
||||||
|
return state, [block], test_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_epoch_transition_not_finalizing(state):
|
||||||
|
test_state = deepcopy(state)
|
||||||
|
block = build_empty_block_for_next_slot(test_state)
|
||||||
|
block.slot += spec.SLOTS_PER_EPOCH * 5
|
||||||
|
|
||||||
|
state_transition(test_state, block)
|
||||||
|
|
||||||
|
assert test_state.slot == block.slot
|
||||||
|
assert test_state.finalized_epoch < get_current_epoch(test_state) - 4
|
||||||
|
for index in range(len(test_state.validator_registry)):
|
||||||
|
assert get_balance(test_state, index) < get_balance(state, index)
|
||||||
|
|
||||||
|
return state, [block], test_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_proposer_slashing(state):
|
||||||
|
test_state = deepcopy(state)
|
||||||
|
proposer_slashing = get_valid_proposer_slashing(state)
|
||||||
|
validator_index = proposer_slashing.proposer_index
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add to state via block transition
|
||||||
|
#
|
||||||
|
block = build_empty_block_for_next_slot(test_state)
|
||||||
|
block.body.proposer_slashings.append(proposer_slashing)
|
||||||
|
state_transition(test_state, block)
|
||||||
|
|
||||||
|
assert not state.validator_registry[validator_index].initiated_exit
|
||||||
|
assert not state.validator_registry[validator_index].slashed
|
||||||
|
|
||||||
|
slashed_validator = test_state.validator_registry[validator_index]
|
||||||
|
assert not slashed_validator.initiated_exit
|
||||||
|
assert slashed_validator.slashed
|
||||||
|
assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
|
||||||
|
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
|
||||||
|
# lost whistleblower reward
|
||||||
|
assert get_balance(test_state, validator_index) < get_balance(state, validator_index)
|
||||||
|
|
||||||
|
return state, [block], test_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_deposit_in_block(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
test_deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry)
|
||||||
|
|
||||||
|
index = len(test_deposit_data_leaves)
|
||||||
|
pubkey = pubkeys[index]
|
||||||
|
privkey = privkeys[index]
|
||||||
|
deposit_data = build_deposit_data(pre_state, pubkey, privkey, spec.MAX_DEPOSIT_AMOUNT)
|
||||||
|
|
||||||
|
item = hash(deposit_data.serialize())
|
||||||
|
test_deposit_data_leaves.append(item)
|
||||||
|
tree = calc_merkle_tree_from_leaves(tuple(test_deposit_data_leaves))
|
||||||
|
root = get_merkle_root((tuple(test_deposit_data_leaves)))
|
||||||
|
proof = list(get_merkle_proof(tree, item_index=index))
|
||||||
|
assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, index, root)
|
||||||
|
|
||||||
|
deposit = Deposit(
|
||||||
|
proof=list(proof),
|
||||||
|
index=index,
|
||||||
|
data=deposit_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
pre_state.latest_eth1_data.deposit_root = root
|
||||||
|
pre_state.latest_eth1_data.deposit_count = len(test_deposit_data_leaves)
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
block = build_empty_block_for_next_slot(post_state)
|
||||||
|
block.body.deposits.append(deposit)
|
||||||
|
|
||||||
|
state_transition(post_state, block)
|
||||||
|
assert len(post_state.validator_registry) == len(state.validator_registry) + 1
|
||||||
|
assert len(post_state.balances) == len(state.balances) + 1
|
||||||
|
assert get_balance(post_state, index) == spec.MAX_DEPOSIT_AMOUNT
|
||||||
|
assert post_state.validator_registry[index].pubkey == pubkeys[index]
|
||||||
|
|
||||||
|
return pre_state, [block], post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_deposit_top_up(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
test_deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry)
|
||||||
|
|
||||||
|
validator_index = 0
|
||||||
|
amount = spec.MAX_DEPOSIT_AMOUNT // 4
|
||||||
|
pubkey = pubkeys[validator_index]
|
||||||
|
privkey = privkeys[validator_index]
|
||||||
|
deposit_data = build_deposit_data(pre_state, pubkey, privkey, amount)
|
||||||
|
|
||||||
|
merkle_index = len(test_deposit_data_leaves)
|
||||||
|
item = hash(deposit_data.serialize())
|
||||||
|
test_deposit_data_leaves.append(item)
|
||||||
|
tree = calc_merkle_tree_from_leaves(tuple(test_deposit_data_leaves))
|
||||||
|
root = get_merkle_root((tuple(test_deposit_data_leaves)))
|
||||||
|
proof = list(get_merkle_proof(tree, item_index=merkle_index))
|
||||||
|
assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, merkle_index, root)
|
||||||
|
|
||||||
|
deposit = Deposit(
|
||||||
|
proof=list(proof),
|
||||||
|
index=merkle_index,
|
||||||
|
data=deposit_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
pre_state.latest_eth1_data.deposit_root = root
|
||||||
|
pre_state.latest_eth1_data.deposit_count = len(test_deposit_data_leaves)
|
||||||
|
block = build_empty_block_for_next_slot(pre_state)
|
||||||
|
block.body.deposits.append(deposit)
|
||||||
|
|
||||||
|
pre_balance = get_balance(pre_state, validator_index)
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
state_transition(post_state, block)
|
||||||
|
assert len(post_state.validator_registry) == len(pre_state.validator_registry)
|
||||||
|
assert len(post_state.balances) == len(pre_state.balances)
|
||||||
|
assert get_balance(post_state, validator_index) == pre_balance + amount
|
||||||
|
|
||||||
|
return pre_state, [block], post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_attestation(state):
|
||||||
|
test_state = deepcopy(state)
|
||||||
|
attestation = get_valid_attestation(state)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add to state via block transition
|
||||||
|
#
|
||||||
|
attestation_block = build_empty_block_for_next_slot(test_state)
|
||||||
|
attestation_block.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY
|
||||||
|
attestation_block.body.attestations.append(attestation)
|
||||||
|
state_transition(test_state, attestation_block)
|
||||||
|
|
||||||
|
assert len(test_state.current_epoch_attestations) == len(state.current_epoch_attestations) + 1
|
||||||
|
|
||||||
|
#
|
||||||
|
# Epoch transition should move to previous_epoch_attestations
|
||||||
|
#
|
||||||
|
pre_current_epoch_attestations = deepcopy(test_state.current_epoch_attestations)
|
||||||
|
|
||||||
|
epoch_block = build_empty_block_for_next_slot(test_state)
|
||||||
|
epoch_block.slot += spec.SLOTS_PER_EPOCH
|
||||||
|
state_transition(test_state, epoch_block)
|
||||||
|
|
||||||
|
assert len(test_state.current_epoch_attestations) == 0
|
||||||
|
assert test_state.previous_epoch_attestations == pre_current_epoch_attestations
|
||||||
|
|
||||||
|
return state, [attestation_block, epoch_block], test_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_voluntary_exit(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
validator_index = get_active_validator_indices(
|
||||||
|
pre_state.validator_registry,
|
||||||
|
get_current_epoch(pre_state)
|
||||||
|
)[-1]
|
||||||
|
|
||||||
|
# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
|
||||||
|
pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
|
||||||
|
# artificially trigger registry update at next epoch transition
|
||||||
|
force_registry_change_at_next_epoch(pre_state)
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
voluntary_exit = VoluntaryExit(
|
||||||
|
epoch=get_current_epoch(pre_state),
|
||||||
|
validator_index=validator_index,
|
||||||
|
signature=EMPTY_SIGNATURE,
|
||||||
|
)
|
||||||
|
voluntary_exit.signature = bls.sign(
|
||||||
|
message_hash=signed_root(voluntary_exit),
|
||||||
|
privkey=privkeys[validator_index],
|
||||||
|
domain=get_domain(
|
||||||
|
fork=pre_state.fork,
|
||||||
|
epoch=get_current_epoch(pre_state),
|
||||||
|
domain_type=spec.DOMAIN_VOLUNTARY_EXIT,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add to state via block transition
|
||||||
|
#
|
||||||
|
initiate_exit_block = build_empty_block_for_next_slot(post_state)
|
||||||
|
initiate_exit_block.body.voluntary_exits.append(voluntary_exit)
|
||||||
|
state_transition(post_state, initiate_exit_block)
|
||||||
|
|
||||||
|
assert not pre_state.validator_registry[validator_index].initiated_exit
|
||||||
|
assert post_state.validator_registry[validator_index].initiated_exit
|
||||||
|
assert post_state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH
|
||||||
|
|
||||||
|
#
|
||||||
|
# Process within epoch transition
|
||||||
|
#
|
||||||
|
exit_block = build_empty_block_for_next_slot(post_state)
|
||||||
|
exit_block.slot += spec.SLOTS_PER_EPOCH
|
||||||
|
state_transition(post_state, exit_block)
|
||||||
|
|
||||||
|
assert post_state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH
|
||||||
|
|
||||||
|
return pre_state, [initiate_exit_block, exit_block], post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_exit_churn_too_long_since_change(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
validator_index = get_active_validator_indices(
|
||||||
|
pre_state.validator_registry,
|
||||||
|
get_current_epoch(pre_state)
|
||||||
|
)[-1]
|
||||||
|
|
||||||
|
#
|
||||||
|
# setup pre_state
|
||||||
|
#
|
||||||
|
# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
|
||||||
|
pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
|
||||||
|
# artificially trigger registry update at next epoch transition
|
||||||
|
force_registry_change_at_next_epoch(pre_state)
|
||||||
|
# make epochs since registry update greater than LATEST_SLASHED_EXIT_LENGTH
|
||||||
|
pre_state.validator_registry_update_epoch = (
|
||||||
|
get_current_epoch(pre_state) - spec.LATEST_SLASHED_EXIT_LENGTH
|
||||||
|
)
|
||||||
|
# set validator to have previously initiated exit
|
||||||
|
pre_state.validator_registry[validator_index].initiated_exit = True
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Process registry change but ensure no exit
|
||||||
|
#
|
||||||
|
block = build_empty_block_for_next_slot(post_state)
|
||||||
|
block.slot += spec.SLOTS_PER_EPOCH
|
||||||
|
state_transition(post_state, block)
|
||||||
|
|
||||||
|
assert post_state.validator_registry_update_epoch == get_current_epoch(post_state) - 1
|
||||||
|
assert post_state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH
|
||||||
|
|
||||||
|
return pre_state, [block], post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_transfer(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
current_epoch = get_current_epoch(pre_state)
|
||||||
|
sender_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[-1]
|
||||||
|
recipient_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0]
|
||||||
|
transfer_pubkey = pubkeys[-1]
|
||||||
|
transfer_privkey = privkeys[-1]
|
||||||
|
amount = get_balance(pre_state, sender_index)
|
||||||
|
pre_transfer_recipient_balance = get_balance(pre_state, recipient_index)
|
||||||
|
transfer = Transfer(
|
||||||
|
sender=sender_index,
|
||||||
|
recipient=recipient_index,
|
||||||
|
amount=amount,
|
||||||
|
fee=0,
|
||||||
|
slot=pre_state.slot + 1,
|
||||||
|
pubkey=transfer_pubkey,
|
||||||
|
signature=EMPTY_SIGNATURE,
|
||||||
|
)
|
||||||
|
transfer.signature = bls.sign(
|
||||||
|
message_hash=signed_root(transfer),
|
||||||
|
privkey=transfer_privkey,
|
||||||
|
domain=get_domain(
|
||||||
|
fork=pre_state.fork,
|
||||||
|
epoch=get_current_epoch(pre_state),
|
||||||
|
domain_type=spec.DOMAIN_TRANSFER,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ensure withdrawal_credentials reproducable
|
||||||
|
pre_state.validator_registry[sender_index].withdrawal_credentials = (
|
||||||
|
spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(transfer_pubkey)[1:]
|
||||||
|
)
|
||||||
|
# un-activate so validator can transfer
|
||||||
|
pre_state.validator_registry[sender_index].activation_epoch = spec.FAR_FUTURE_EPOCH
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
#
|
||||||
|
# Add to state via block transition
|
||||||
|
#
|
||||||
|
block = build_empty_block_for_next_slot(post_state)
|
||||||
|
block.body.transfers.append(transfer)
|
||||||
|
state_transition(post_state, block)
|
||||||
|
|
||||||
|
sender_balance = get_balance(post_state, sender_index)
|
||||||
|
recipient_balance = get_balance(post_state, recipient_index)
|
||||||
|
assert sender_balance == 0
|
||||||
|
assert recipient_balance == pre_transfer_recipient_balance + amount
|
||||||
|
|
||||||
|
return pre_state, [block], post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_ejection(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
|
||||||
|
current_epoch = get_current_epoch(pre_state)
|
||||||
|
validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[-1]
|
||||||
|
|
||||||
|
assert pre_state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH
|
||||||
|
|
||||||
|
# set validator balance to below ejection threshold
|
||||||
|
set_balance(pre_state, validator_index, spec.EJECTION_BALANCE - 1)
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
#
|
||||||
|
# trigger epoch transition
|
||||||
|
#
|
||||||
|
block = build_empty_block_for_next_slot(post_state)
|
||||||
|
block.slot += spec.SLOTS_PER_EPOCH
|
||||||
|
state_transition(post_state, block)
|
||||||
|
|
||||||
|
assert post_state.validator_registry[validator_index].initiated_exit == True
|
||||||
|
|
||||||
|
return pre_state, [block], post_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_historical_batch(state):
|
||||||
|
pre_state = deepcopy(state)
|
||||||
|
pre_state.slot += spec.SLOTS_PER_HISTORICAL_ROOT - (pre_state.slot % spec.SLOTS_PER_HISTORICAL_ROOT) - 1
|
||||||
|
|
||||||
|
post_state = deepcopy(pre_state)
|
||||||
|
|
||||||
|
block = build_empty_block_for_next_slot(post_state)
|
||||||
|
|
||||||
|
state_transition(post_state, block)
|
||||||
|
|
||||||
|
assert post_state.slot == block.slot
|
||||||
|
assert get_current_epoch(post_state) % (spec.SLOTS_PER_HISTORICAL_ROOT // spec.SLOTS_PER_EPOCH) == 0
|
||||||
|
assert len(post_state.historical_roots) == len(pre_state.historical_roots) + 1
|
||||||
|
|
||||||
|
return pre_state, [block], post_state
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
def bls_verify(pubkey, message_hash, signature, domain):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def bls_verify_multiple(pubkeys, message_hashes, signature, domain):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def bls_aggregate_pubkeys(pubkeys):
|
||||||
|
return b'\x42' * 96
|
|
@ -0,0 +1,6 @@
|
||||||
|
from hashlib import sha256
|
||||||
|
# from eth_utils import keccak
|
||||||
|
|
||||||
|
|
||||||
|
def hash(x): return sha256(x).digest()
|
||||||
|
# def hash(x): return keccak(x)
|
|
@ -0,0 +1,52 @@
|
||||||
|
from .minimal_ssz import hash_tree_root
|
||||||
|
|
||||||
|
|
||||||
|
def jsonize(value, typ, include_hash_tree_roots=False):
|
||||||
|
if isinstance(typ, str) and typ[:4] == 'uint':
|
||||||
|
return value
|
||||||
|
elif typ == 'bool':
|
||||||
|
assert value in (True, False)
|
||||||
|
return value
|
||||||
|
elif isinstance(typ, list):
|
||||||
|
return [jsonize(element, typ[0], include_hash_tree_roots) for element in value]
|
||||||
|
elif isinstance(typ, str) and typ[:4] == 'byte':
|
||||||
|
return '0x' + value.hex()
|
||||||
|
elif hasattr(typ, 'fields'):
|
||||||
|
ret = {}
|
||||||
|
for field, subtype in typ.fields.items():
|
||||||
|
ret[field] = jsonize(getattr(value, field), subtype, include_hash_tree_roots)
|
||||||
|
if include_hash_tree_roots:
|
||||||
|
ret[field + "_hash_tree_root"] = '0x' + hash_tree_root(getattr(value, field), subtype).hex()
|
||||||
|
if include_hash_tree_roots:
|
||||||
|
ret["hash_tree_root"] = '0x' + hash_tree_root(value, typ).hex()
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
print(value, typ)
|
||||||
|
raise Exception("Type not recognized")
|
||||||
|
|
||||||
|
|
||||||
|
def dejsonize(json, typ):
|
||||||
|
if isinstance(typ, str) and typ[:4] == 'uint':
|
||||||
|
return json
|
||||||
|
elif typ == 'bool':
|
||||||
|
assert json in (True, False)
|
||||||
|
return json
|
||||||
|
elif isinstance(typ, list):
|
||||||
|
return [dejsonize(element, typ[0]) for element in json]
|
||||||
|
elif isinstance(typ, str) and typ[:4] == 'byte':
|
||||||
|
return bytes.fromhex(json[2:])
|
||||||
|
elif hasattr(typ, 'fields'):
|
||||||
|
temp = {}
|
||||||
|
for field, subtype in typ.fields.items():
|
||||||
|
temp[field] = dejsonize(json[field], subtype)
|
||||||
|
if field + "_hash_tree_root" in json:
|
||||||
|
assert(json[field + "_hash_tree_root"][2:] ==
|
||||||
|
hash_tree_root(temp[field], subtype).hex())
|
||||||
|
ret = typ(**temp)
|
||||||
|
if "hash_tree_root" in json:
|
||||||
|
assert(json["hash_tree_root"][2:] ==
|
||||||
|
hash_tree_root(ret, typ).hex())
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
print(json, typ)
|
||||||
|
raise Exception("Type not recognized")
|
|
@ -0,0 +1,30 @@
|
||||||
|
from .hash_function import hash
|
||||||
|
|
||||||
|
|
||||||
|
zerohashes = [b'\x00' * 32]
|
||||||
|
for layer in range(1, 32):
|
||||||
|
zerohashes.append(hash(zerohashes[layer - 1] + zerohashes[layer - 1]))
|
||||||
|
|
||||||
|
|
||||||
|
# Compute a Merkle root of a right-zerobyte-padded 2**32 sized tree
|
||||||
|
def calc_merkle_tree_from_leaves(values):
|
||||||
|
values = list(values)
|
||||||
|
tree = [values[::]]
|
||||||
|
for h in range(32):
|
||||||
|
if len(values) % 2 == 1:
|
||||||
|
values.append(zerohashes[h])
|
||||||
|
values = [hash(values[i] + values[i + 1]) for i in range(0, len(values), 2)]
|
||||||
|
tree.append(values[::])
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
def get_merkle_root(values):
|
||||||
|
return calc_merkle_tree_from_leaves(values)[-1][0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_merkle_proof(tree, item_index):
|
||||||
|
proof = []
|
||||||
|
for i in range(32):
|
||||||
|
subindex = (item_index // 2**i) ^ 1
|
||||||
|
proof.append(tree[i][subindex] if subindex < len(tree[i]) else zerohashes[i])
|
||||||
|
return proof
|
|
@ -0,0 +1,213 @@
|
||||||
|
from .hash_function import hash
|
||||||
|
|
||||||
|
|
||||||
|
BYTES_PER_CHUNK = 32
|
||||||
|
BYTES_PER_LENGTH_PREFIX = 4
|
||||||
|
ZERO_CHUNK = b'\x00' * BYTES_PER_CHUNK
|
||||||
|
|
||||||
|
|
||||||
|
def SSZType(fields):
|
||||||
|
class SSZObject():
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for f in fields:
|
||||||
|
if f not in kwargs:
|
||||||
|
raise Exception("Missing constructor argument: %s" % f)
|
||||||
|
setattr(self, f, kwargs[f])
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
self.fields == other.fields and
|
||||||
|
self.serialize() == other.serialize()
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return int.from_bytes(self.hash_tree_root(), byteorder="little")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
output = []
|
||||||
|
for field in self.fields:
|
||||||
|
output.append(f'{field}: {getattr(self, field)}')
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return serialize_value(self, self.__class__)
|
||||||
|
|
||||||
|
def hash_tree_root(self):
|
||||||
|
return hash_tree_root(self, self.__class__)
|
||||||
|
|
||||||
|
SSZObject.fields = fields
|
||||||
|
return SSZObject
|
||||||
|
|
||||||
|
|
||||||
|
class Vector():
|
||||||
|
def __init__(self, items):
|
||||||
|
self.items = items
|
||||||
|
self.length = len(items)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.items[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.items[key] = value
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.items)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.length
|
||||||
|
|
||||||
|
|
||||||
|
def is_basic(typ):
|
||||||
|
return isinstance(typ, str) and (typ[:4] in ('uint', 'bool') or typ == 'byte')
|
||||||
|
|
||||||
|
|
||||||
|
def is_constant_sized(typ):
|
||||||
|
if is_basic(typ):
|
||||||
|
return True
|
||||||
|
elif isinstance(typ, list) and len(typ) == 1:
|
||||||
|
return is_constant_sized(typ[0])
|
||||||
|
elif isinstance(typ, list) and len(typ) == 2:
|
||||||
|
return False
|
||||||
|
elif isinstance(typ, str) and typ[:5] == 'bytes':
|
||||||
|
return len(typ) > 5
|
||||||
|
elif hasattr(typ, 'fields'):
|
||||||
|
for subtype in typ.fields.values():
|
||||||
|
if not is_constant_sized(subtype):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception("Type not recognized")
|
||||||
|
|
||||||
|
|
||||||
|
def coerce_to_bytes(x):
|
||||||
|
if isinstance(x, str):
|
||||||
|
o = x.encode('utf-8')
|
||||||
|
assert len(o) == len(x)
|
||||||
|
return o
|
||||||
|
elif isinstance(x, bytes):
|
||||||
|
return x
|
||||||
|
else:
|
||||||
|
raise Exception("Expecting bytes")
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_value(value, typ=None):
|
||||||
|
if typ is None:
|
||||||
|
typ = infer_type(value)
|
||||||
|
if isinstance(typ, str) and typ[:4] == 'uint':
|
||||||
|
length = int(typ[4:])
|
||||||
|
assert length in (8, 16, 32, 64, 128, 256)
|
||||||
|
return value.to_bytes(length // 8, 'little')
|
||||||
|
elif typ == 'bool':
|
||||||
|
assert value in (True, False)
|
||||||
|
return b'\x01' if value is True else b'\x00'
|
||||||
|
elif (isinstance(typ, list) and len(typ) == 1) or typ == 'bytes':
|
||||||
|
serialized_bytes = coerce_to_bytes(value) if typ == 'bytes' else b''.join([serialize_value(element, typ[0]) for element in value])
|
||||||
|
assert len(serialized_bytes) < 2**(8 * BYTES_PER_LENGTH_PREFIX)
|
||||||
|
serialized_length = len(serialized_bytes).to_bytes(BYTES_PER_LENGTH_PREFIX, 'little')
|
||||||
|
return serialized_length + serialized_bytes
|
||||||
|
elif isinstance(typ, list) and len(typ) == 2:
|
||||||
|
assert len(value) == typ[1]
|
||||||
|
return b''.join([serialize_value(element, typ[0]) for element in value])
|
||||||
|
elif isinstance(typ, str) and len(typ) > 5 and typ[:5] == 'bytes':
|
||||||
|
assert len(value) == int(typ[5:]), (value, int(typ[5:]))
|
||||||
|
return coerce_to_bytes(value)
|
||||||
|
elif hasattr(typ, 'fields'):
|
||||||
|
serialized_bytes = b''.join([serialize_value(getattr(value, field), subtype) for field, subtype in typ.fields.items()])
|
||||||
|
if is_constant_sized(typ):
|
||||||
|
return serialized_bytes
|
||||||
|
else:
|
||||||
|
assert len(serialized_bytes) < 2**(8 * BYTES_PER_LENGTH_PREFIX)
|
||||||
|
serialized_length = len(serialized_bytes).to_bytes(BYTES_PER_LENGTH_PREFIX, 'little')
|
||||||
|
return serialized_length + serialized_bytes
|
||||||
|
else:
|
||||||
|
print(value, typ)
|
||||||
|
raise Exception("Type not recognized")
|
||||||
|
|
||||||
|
|
||||||
|
def chunkify(bytez):
|
||||||
|
bytez += b'\x00' * (-len(bytez) % BYTES_PER_CHUNK)
|
||||||
|
return [bytez[i:i + 32] for i in range(0, len(bytez), 32)]
|
||||||
|
|
||||||
|
|
||||||
|
def pack(values, subtype):
|
||||||
|
return chunkify(b''.join([serialize_value(value, subtype) for value in values]))
|
||||||
|
|
||||||
|
|
||||||
|
def is_power_of_two(x):
|
||||||
|
return x > 0 and x & (x - 1) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def merkleize(chunks):
|
||||||
|
tree = chunks[::]
|
||||||
|
while not is_power_of_two(len(tree)):
|
||||||
|
tree.append(ZERO_CHUNK)
|
||||||
|
tree = [ZERO_CHUNK] * len(tree) + tree
|
||||||
|
for i in range(len(tree) // 2 - 1, 0, -1):
|
||||||
|
tree[i] = hash(tree[i * 2] + tree[i * 2 + 1])
|
||||||
|
return tree[1]
|
||||||
|
|
||||||
|
|
||||||
|
def mix_in_length(root, length):
|
||||||
|
return hash(root + length.to_bytes(32, 'little'))
|
||||||
|
|
||||||
|
|
||||||
|
def infer_type(value):
|
||||||
|
if hasattr(value.__class__, 'fields'):
|
||||||
|
return value.__class__
|
||||||
|
elif isinstance(value, Vector):
|
||||||
|
return [infer_type(value[0]) if len(value) > 0 else 'uint64', len(value)]
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return [infer_type(value[0])] if len(value) > 0 else ['uint64']
|
||||||
|
elif isinstance(value, (bytes, str)):
|
||||||
|
return 'bytes'
|
||||||
|
elif isinstance(value, int):
|
||||||
|
return 'uint64'
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to infer type")
|
||||||
|
|
||||||
|
|
||||||
|
def hash_tree_root(value, typ=None):
|
||||||
|
if typ is None:
|
||||||
|
typ = infer_type(value)
|
||||||
|
if is_basic(typ):
|
||||||
|
return merkleize(pack([value], typ))
|
||||||
|
elif isinstance(typ, list) and len(typ) == 1 and is_basic(typ[0]):
|
||||||
|
return mix_in_length(merkleize(pack(value, typ[0])), len(value))
|
||||||
|
elif isinstance(typ, list) and len(typ) == 1 and not is_basic(typ[0]):
|
||||||
|
return mix_in_length(merkleize([hash_tree_root(element, typ[0]) for element in value]), len(value))
|
||||||
|
elif isinstance(typ, list) and len(typ) == 2 and is_basic(typ[0]):
|
||||||
|
assert len(value) == typ[1]
|
||||||
|
return merkleize(pack(value, typ[0]))
|
||||||
|
elif typ == 'bytes':
|
||||||
|
return mix_in_length(merkleize(chunkify(coerce_to_bytes(value))), len(value))
|
||||||
|
elif isinstance(typ, str) and typ[:5] == 'bytes' and len(typ) > 5:
|
||||||
|
assert len(value) == int(typ[5:])
|
||||||
|
return merkleize(chunkify(coerce_to_bytes(value)))
|
||||||
|
elif isinstance(typ, list) and len(typ) == 2 and not is_basic(typ[0]):
|
||||||
|
return merkleize([hash_tree_root(element, typ[0]) for element in value])
|
||||||
|
elif hasattr(typ, 'fields'):
|
||||||
|
return merkleize([hash_tree_root(getattr(value, field), subtype) for field, subtype in typ.fields.items()])
|
||||||
|
else:
|
||||||
|
raise Exception("Type not recognized")
|
||||||
|
|
||||||
|
|
||||||
|
def truncate(container):
|
||||||
|
field_keys = list(container.fields.keys())
|
||||||
|
truncated_fields = {
|
||||||
|
key: container.fields[key]
|
||||||
|
for key in field_keys[:-1]
|
||||||
|
}
|
||||||
|
truncated_class = SSZType(truncated_fields)
|
||||||
|
kwargs = {
|
||||||
|
field: getattr(container, field)
|
||||||
|
for field in field_keys[:-1]
|
||||||
|
}
|
||||||
|
return truncated_class(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def signed_root(container):
|
||||||
|
return hash_tree_root(truncate(container))
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(ssz_object):
|
||||||
|
return getattr(ssz_object, 'serialize')()
|
|
@ -0,0 +1,113 @@
|
||||||
|
from . import spec
|
||||||
|
|
||||||
|
|
||||||
|
from typing import ( # noqa: F401
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
List,
|
||||||
|
NewType,
|
||||||
|
Tuple,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .spec import (
|
||||||
|
BeaconState,
|
||||||
|
BeaconBlock,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def expected_deposit_count(state: BeaconState) -> int:
|
||||||
|
return min(
|
||||||
|
spec.MAX_DEPOSITS,
|
||||||
|
state.latest_eth1_data.deposit_count - state.deposit_index
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_transaction_type(state: BeaconState,
|
||||||
|
transactions: List[Any],
|
||||||
|
max_transactions: int,
|
||||||
|
tx_fn: Callable[[BeaconState, Any], None]) -> None:
|
||||||
|
assert len(transactions) <= max_transactions
|
||||||
|
for transaction in transactions:
|
||||||
|
tx_fn(state, transaction)
|
||||||
|
|
||||||
|
|
||||||
|
def process_transactions(state: BeaconState, block: BeaconBlock) -> None:
|
||||||
|
process_transaction_type(
|
||||||
|
state,
|
||||||
|
block.body.proposer_slashings,
|
||||||
|
spec.MAX_PROPOSER_SLASHINGS,
|
||||||
|
spec.process_proposer_slashing,
|
||||||
|
)
|
||||||
|
|
||||||
|
process_transaction_type(
|
||||||
|
state,
|
||||||
|
block.body.attester_slashings,
|
||||||
|
spec.MAX_ATTESTER_SLASHINGS,
|
||||||
|
spec.process_attester_slashing,
|
||||||
|
)
|
||||||
|
|
||||||
|
process_transaction_type(
|
||||||
|
state,
|
||||||
|
block.body.attestations,
|
||||||
|
spec.MAX_ATTESTATIONS,
|
||||||
|
spec.process_attestation,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(block.body.deposits) == expected_deposit_count(state)
|
||||||
|
process_transaction_type(
|
||||||
|
state,
|
||||||
|
block.body.deposits,
|
||||||
|
spec.MAX_DEPOSITS,
|
||||||
|
spec.process_deposit,
|
||||||
|
)
|
||||||
|
|
||||||
|
process_transaction_type(
|
||||||
|
state,
|
||||||
|
block.body.voluntary_exits,
|
||||||
|
spec.MAX_VOLUNTARY_EXITS,
|
||||||
|
spec.process_voluntary_exit,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(block.body.transfers) == len(set(block.body.transfers))
|
||||||
|
process_transaction_type(
|
||||||
|
state,
|
||||||
|
block.body.transfers,
|
||||||
|
spec.MAX_TRANSFERS,
|
||||||
|
spec.process_transfer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_block(state: BeaconState,
|
||||||
|
block: BeaconBlock,
|
||||||
|
verify_state_root: bool=False) -> None:
|
||||||
|
spec.process_block_header(state, block)
|
||||||
|
spec.process_randao(state, block)
|
||||||
|
spec.process_eth1_data(state, block)
|
||||||
|
|
||||||
|
process_transactions(state, block)
|
||||||
|
if verify_state_root:
|
||||||
|
spec.verify_block_state_root(state, block)
|
||||||
|
|
||||||
|
|
||||||
|
def process_epoch_transition(state: BeaconState) -> None:
|
||||||
|
spec.update_justification_and_finalization(state)
|
||||||
|
spec.process_crosslinks(state)
|
||||||
|
spec.maybe_reset_eth1_period(state)
|
||||||
|
spec.apply_rewards(state)
|
||||||
|
spec.process_ejections(state)
|
||||||
|
spec.update_registry(state)
|
||||||
|
spec.process_slashings(state)
|
||||||
|
spec.process_exit_queue(state)
|
||||||
|
spec.finish_epoch_update(state)
|
||||||
|
|
||||||
|
|
||||||
|
def state_transition(state: BeaconState,
|
||||||
|
block: BeaconBlock,
|
||||||
|
verify_state_root: bool=False) -> BeaconState:
|
||||||
|
while state.slot < block.slot:
|
||||||
|
spec.cache_state(state)
|
||||||
|
if (state.slot + 1) % spec.SLOTS_PER_EPOCH == 0:
|
||||||
|
process_epoch_transition(state)
|
||||||
|
spec.advance_slot(state)
|
||||||
|
if block.slot == state.slot:
|
||||||
|
process_block(state, block, verify_state_root)
|
Loading…
Reference in New Issue