Merge branch 'dev' into vbuterin-patch-7
This commit is contained in:
commit
dcb0205adc
|
@ -1453,7 +1453,7 @@ def get_genesis_beacon_state(genesis_validator_deposits: List[Deposit],
|
|||
validator_registry_update_epoch=GENESIS_EPOCH,
|
||||
|
||||
# Randomness and committees
|
||||
latest_randao_mixes=[EMPTY_SIGNATURE for _ in range(LATEST_RANDAO_MIXES_LENGTH)],
|
||||
latest_randao_mixes=[ZERO_HASH for _ in range(LATEST_RANDAO_MIXES_LENGTH)],
|
||||
previous_shuffling_start_shard=GENESIS_START_SHARD,
|
||||
current_shuffling_start_shard=GENESIS_START_SHARD,
|
||||
previous_shuffling_epoch=GENESIS_EPOCH,
|
||||
|
@ -1632,59 +1632,123 @@ Below are the processing steps that happen at every `block`.
|
|||
|
||||
Verify that `len(block.body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS`.
|
||||
|
||||
For each `proposer_slashing` in `block.body.proposer_slashings`:
|
||||
For each `proposer_slashing` in `block.body.proposer_slashings`, run the following function:
|
||||
|
||||
* Let `proposer = state.validator_registry[proposer_slashing.proposer_index]`.
|
||||
* Verify that `proposer_slashing.proposal_1.slot == proposer_slashing.proposal_2.slot`.
|
||||
* Verify that `proposer_slashing.proposal_1.shard == proposer_slashing.proposal_2.shard`.
|
||||
* Verify that `proposer_slashing.proposal_1.block_root != proposer_slashing.proposal_2.block_root`.
|
||||
* Verify that `proposer.slashed is False`.
|
||||
* Verify that `bls_verify(pubkey=proposer.pubkey, message_hash=signed_root(proposer_slashing.proposal_1, "signature"), signature=proposer_slashing.proposal_1.signature, domain=get_domain(state.fork, slot_to_epoch(proposer_slashing.proposal_1.slot), DOMAIN_PROPOSAL))`.
|
||||
* Verify that `bls_verify(pubkey=proposer.pubkey, message_hash=signed_root(proposer_slashing.proposal_2, "signature"), signature=proposer_slashing.proposal_2.signature, domain=get_domain(state.fork, slot_to_epoch(proposer_slashing.proposal_2.slot), DOMAIN_PROPOSAL))`.
|
||||
* Run `slash_validator(state, proposer_slashing.proposer_index)`.
|
||||
```python
|
||||
def process_proposer_slashing(state: BeaconState,
|
||||
proposer_slashing: ProposerSlashing) -> None:
|
||||
"""
|
||||
Process ``ProposerSlashing`` transaction.
|
||||
Note that this function mutates ``state``.
|
||||
"""
|
||||
proposer = state.validator_registry[proposer_slashing.proposer_index]
|
||||
# Verify that the slot is the same
|
||||
assert proposer_slashing.proposal_1.slot == proposer_slashing.proposal_2.slot
|
||||
# Verify that the shard is the same (or that both proposals are beacon chain proposals)
|
||||
assert proposer_slashing.proposal_1.shard == proposer_slashing.proposal_2.shard
|
||||
# But the roots are different!
|
||||
assert proposer_slashing.proposal_1.block_root != proposer_slashing.proposal_2.block_root
|
||||
# Proposer is not yet slashed
|
||||
assert proposer.slashed is False
|
||||
# Signatures are valid
|
||||
for proposal in (proposer_slashing.proposal_1, proposer_slashing.proposal_2):
|
||||
assert bls_verify(
|
||||
pubkey=proposer.pubkey,
|
||||
message_hash=signed_root(proposal, "signature"),
|
||||
signature=proposal.signature,
|
||||
domain=get_domain(state.fork, slot_to_epoch(proposal.slot), DOMAIN_PROPOSAL)
|
||||
)
|
||||
slash_validator(state, proposer_slashing.proposer_index)
|
||||
```
|
||||
|
||||
##### Attester slashings
|
||||
|
||||
Verify that `len(block.body.attester_slashings) <= MAX_ATTESTER_SLASHINGS`.
|
||||
|
||||
For each `attester_slashing` in `block.body.attester_slashings`:
|
||||
For each `attester_slashing` in `block.body.attester_slashings`, run the following function:
|
||||
|
||||
* Let `slashable_attestation_1 = attester_slashing.slashable_attestation_1`.
|
||||
* Let `slashable_attestation_2 = attester_slashing.slashable_attestation_2`.
|
||||
* Verify that `slashable_attestation_1.data != slashable_attestation_2.data`.
|
||||
* Verify that `is_double_vote(slashable_attestation_1.data, slashable_attestation_2.data)` or `is_surround_vote(slashable_attestation_1.data, slashable_attestation_2.data)`.
|
||||
* Verify that `verify_slashable_attestation(state, slashable_attestation_1)`.
|
||||
* Verify that `verify_slashable_attestation(state, slashable_attestation_2)`.
|
||||
* Let `slashable_indices = [index for index in slashable_attestation_1.validator_indices if index in slashable_attestation_2.validator_indices and state.validator_registry[index].slashed is False]`.
|
||||
* Verify that `len(slashable_indices) >= 1`.
|
||||
* Run `slash_validator(state, index)` for each `index` in `slashable_indices`.
|
||||
```python
|
||||
def process_attester_slashing(state: BeaconState,
|
||||
attester_slashing: AttesterSlashing) -> None:
|
||||
"""
|
||||
Process ``AttesterSlashing`` transaction.
|
||||
Note that this function mutates ``state``.
|
||||
"""
|
||||
attestation1 = attester_slashing.slashable_attestation_1
|
||||
attestation2 = attester_slashing.slashable_attestation_2
|
||||
# Check that the attestations are conflicting
|
||||
assert attestation1.data != attestation2.data
|
||||
assert (
|
||||
is_double_vote(attestation1.data, attestation2.data) or
|
||||
is_surround_vote(attestation1.data, attestation2.data)
|
||||
)
|
||||
assert verify_slashable_attestation(state, attestation1)
|
||||
assert verify_slashable_attestation(state, attestation2)
|
||||
slashable_indices = [
|
||||
index for index in attestation1.validator_indices
|
||||
if (
|
||||
index in attestation2.validator_indices and
|
||||
state.validator_registry[index].slashed is False
|
||||
)
|
||||
]
|
||||
assert len(slashable_indices) >= 1
|
||||
for index in slashable_indices:
|
||||
slash_validator(state, index)
|
||||
```
|
||||
|
||||
##### Attestations
|
||||
|
||||
Verify that `len(block.body.attestations) <= MAX_ATTESTATIONS`.
|
||||
|
||||
For each `attestation` in `block.body.attestations`:
|
||||
|
||||
* Verify that `attestation.data.slot >= GENESIS_SLOT`.
|
||||
* Verify that `attestation.data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot`.
|
||||
* Verify that `state.slot < attestation.data.slot + SLOTS_PER_EPOCH.
|
||||
* Verify that `attestation.data.justified_epoch` is equal to `state.justified_epoch if slot_to_epoch(attestation.data.slot + 1) >= get_current_epoch(state) else state.previous_justified_epoch`.
|
||||
* Verify that `attestation.data.justified_block_root` is equal to `get_block_root(state, get_epoch_start_slot(attestation.data.justified_epoch))`.
|
||||
* Verify that either (i) `state.latest_crosslinks[attestation.data.shard] == attestation.data.latest_crosslink` or (ii) `state.latest_crosslinks[attestation.data.shard] == Crosslink(crosslink_data_root=attestation.data.crosslink_data_root, epoch=slot_to_epoch(attestation.data.slot))`.
|
||||
* Verify bitfields and aggregate signature:
|
||||
For each `attestation` in `block.body.attestations`, run the following function:
|
||||
|
||||
```python
|
||||
assert attestation.custody_bitfield == b'\x00' * len(attestation.custody_bitfield) # [TO BE REMOVED IN PHASE 1]
|
||||
def process_attestation(state: BeaconState, attestation: Attestation) -> None:
|
||||
"""
|
||||
Process ``Attestation`` transaction.
|
||||
Note that this function mutates ``state``.
|
||||
"""
|
||||
# Can't submit attestations that are too far in history (or in prehistory)
|
||||
assert attestation.data.slot >= GENESIS_SLOT
|
||||
assert state.slot < attestation.data.slot + SLOTS_PER_EPOCH
|
||||
# Can't submit attestations too quickly
|
||||
assert attestation.data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot
|
||||
# Verify that the justified epoch is correct, case 1: current epoch attestations
|
||||
if slot_to_epoch(attestation.data.slot + 1) >= get_current_epoch(state):
|
||||
assert attestation.data.justified_epoch == state.justified_epoch
|
||||
# Case 2: previous epoch attestations
|
||||
else:
|
||||
assert attestation.data.justified_epoch == state.previous_justified_epoch
|
||||
# Check that the justified block root is correct
|
||||
assert attestation.data.justified_block_root == get_block_root(
|
||||
state, get_epoch_start_slot(attestation.data.justified_epoch)
|
||||
)
|
||||
# Check that the crosslink data is valid
|
||||
acceptable_crosslink_data = {
|
||||
# Case 1: Latest crosslink matches the one in the state
|
||||
attestation.data.latest_crosslink,
|
||||
# Case 2: State has already been updated, state's latest crosslink matches the crosslink
|
||||
# the attestation is trying to create
|
||||
Crosslink(
|
||||
crosslink_data_root=attestation.data.crosslink_data_root,
|
||||
epoch=slot_to_epoch(attestation.data.slot)
|
||||
)
|
||||
}
|
||||
assert state.latest_crosslinks[attestation.data.shard] in acceptable_crosslink_data
|
||||
# Attestation must be nonempty!
|
||||
assert attestation.aggregation_bitfield != b'\x00' * len(attestation.aggregation_bitfield)
|
||||
|
||||
# Custody must be empty (to be removed in phase 1)
|
||||
assert attestation.custody_bitfield == b'\x00' * len(attestation.custody_bitfield)
|
||||
# Get the committee for the specific shard that this attestation is for
|
||||
crosslink_committee = [
|
||||
committee for committee, shard in get_crosslink_committees_at_slot(state, attestation.data.slot)
|
||||
if shard == attestation.data.shard
|
||||
][0]
|
||||
# Custody bitfield must be a subset of the attestation bitfield
|
||||
for i in range(len(crosslink_committee)):
|
||||
if get_bitfield_bit(attestation.aggregation_bitfield, i) == 0b0:
|
||||
assert get_bitfield_bit(attestation.custody_bitfield, i) == 0b0
|
||||
|
||||
# Verify aggregate signature
|
||||
participants = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield)
|
||||
custody_bit_1_participants = get_attestation_participants(state, attestation.data, attestation.custody_bitfield)
|
||||
custody_bit_0_participants = [i in participants for i not in custody_bit_1_participants]
|
||||
|
@ -1701,13 +1765,21 @@ For each `attestation` in `block.body.attestations`:
|
|||
signature=attestation.aggregate_signature,
|
||||
domain=get_domain(state.fork, slot_to_epoch(attestation.data.slot), DOMAIN_ATTESTATION),
|
||||
)
|
||||
# Crosslink data root is zero (to be removed in phase 1)
|
||||
assert attestation.data.crosslink_data_root == ZERO_HASH
|
||||
# Apply the attestation
|
||||
pending_attestation = PendingAttestation(
|
||||
data=attestation.data,
|
||||
aggregation_bitfield=attestation.aggregation_bitfield,
|
||||
custody_bitfield=attestation.custody_bitfield,
|
||||
inclusion_slot=state.slot
|
||||
)
|
||||
if slot_to_epoch(attestation.data.slot) == get_current_epoch(state):
|
||||
state.current_epoch_attestations.append(pending_attestation)
|
||||
elif slot_to_epoch(attestation.data.slot) == get_previous_epoch(state):
|
||||
state.previous_epoch_attestations.append(pending_attestation)
|
||||
```
|
||||
|
||||
* [TO BE REMOVED IN PHASE 1] Verify that `attestation.data.crosslink_data_root == ZERO_HASH`.
|
||||
* Let `pending_attestation = PendingAttestation(data=attestation.data, aggregation_bitfield=attestation.aggregation_bitfield, custody_bitfield=attestation.custody_bitfield, inclusion_slot=state.slot)`.
|
||||
* Append `pending_attestation` to `state.previous_epoch_attestations` if `slot_to_epoch(attestation.data.slot) == get_previous_epoch(state)`.
|
||||
* Append `pending_attestation` to `state.current_epoch_attestations` if `slot_to_epoch(attestation.data.slot) == get_current_epoch(state)`.
|
||||
|
||||
##### Deposits
|
||||
|
||||
Verify that `len(block.body.deposits) <= MAX_DEPOSITS`.
|
||||
|
@ -1746,13 +1818,29 @@ process_deposit(state, deposit)
|
|||
|
||||
Verify that `len(block.body.voluntary_exits) <= MAX_VOLUNTARY_EXITS`.
|
||||
|
||||
For each `exit` in `block.body.voluntary_exits`:
|
||||
For each `exit` in `block.body.voluntary_exits`, run the following function:
|
||||
|
||||
* Let `validator = state.validator_registry[exit.validator_index]`.
|
||||
* Verify that `validator.exit_epoch > get_delayed_activation_exit_epoch(get_current_epoch(state))`.
|
||||
* Verify that `get_current_epoch(state) >= exit.epoch`.
|
||||
* Verify that `bls_verify(pubkey=validator.pubkey, message_hash=signed_root(exit, "signature"), signature=exit.signature, domain=get_domain(state.fork, exit.epoch, DOMAIN_EXIT))`.
|
||||
* Run `initiate_validator_exit(state, exit.validator_index)`.
|
||||
```python
|
||||
def process_exit(state: BeaconState, exit: VoluntaryExit) -> None:
|
||||
"""
|
||||
Process ``VoluntaryExit`` transaction.
|
||||
Note that this function mutates ``state``.
|
||||
"""
|
||||
validator = state.validator_registry[exit.validator_index]
|
||||
# Verify the validator has not yet exited
|
||||
assert validator.exit_epoch > get_delayed_activation_exit_epoch(get_current_epoch(state))
|
||||
# Exits must specify an epoch when they become valid; they are not valid before then
|
||||
assert get_current_epoch(state) >= exit.epoch
|
||||
# Verify signature
|
||||
assert bls_verify(
|
||||
pubkey=validator.pubkey,
|
||||
message_hash=signed_root(exit, "signature"),
|
||||
signature=exit.signature,
|
||||
domain=get_domain(state.fork, exit.epoch, DOMAIN_EXIT)
|
||||
)
|
||||
# Run the exit
|
||||
initiate_validator_exit(state, exit.validator_index)
|
||||
```
|
||||
|
||||
##### Transfers
|
||||
|
||||
|
@ -1760,18 +1848,46 @@ Note: Transfers are a temporary functionality for phases 0 and 1, to be removed
|
|||
|
||||
Verify that `len(block.body.transfers) <= MAX_TRANSFERS` and that all transfers are distinct.
|
||||
|
||||
For each `transfer` in `block.body.transfers`:
|
||||
For each `transfer` in `block.body.transfers`, run the following function:
|
||||
|
||||
* Verify that `state.validator_balances[transfer.from] >= transfer.amount`.
|
||||
* Verify that `state.validator_balances[transfer.from] >= transfer.fee`.
|
||||
* Verify that `state.validator_balances[transfer.from] == transfer.amount + transfer.fee` or `state.validator_balances[transfer.from] >= transfer.amount + transfer.fee + MIN_DEPOSIT_AMOUNT`.
|
||||
* Verify that `state.slot == transfer.slot`.
|
||||
* Verify that `get_current_epoch(state) >= state.validator_registry[transfer.from].withdrawable_epoch` or `state.validator_registry[transfer.from].activation_epoch == FAR_FUTURE_EPOCH`.
|
||||
* Verify that `state.validator_registry[transfer.from].withdrawal_credentials == BLS_WITHDRAWAL_PREFIX_BYTE + hash(transfer.pubkey)[1:]`.
|
||||
* Verify that `bls_verify(pubkey=transfer.pubkey, message_hash=signed_root(transfer, "signature"), signature=transfer.signature, domain=get_domain(state.fork, slot_to_epoch(transfer.slot), DOMAIN_TRANSFER))`.
|
||||
* Set `state.validator_balances[transfer.from] -= transfer.amount + transfer.fee`.
|
||||
* Set `state.validator_balances[transfer.to] += transfer.amount`.
|
||||
* Set `state.validator_balances[get_beacon_proposer_index(state, state.slot)] += transfer.fee`.
|
||||
```python
|
||||
def process_transfer(state: BeaconState, transfer: Transfer) -> None:
|
||||
"""
|
||||
Process ``Transfer`` transaction.
|
||||
Note that this function mutates ``state``.
|
||||
"""
|
||||
# Verify the amount and fee aren't individually too big (for anti-overflow purposes)
|
||||
assert state.validator_balances[transfer.from] >= max(transfer.amount, transfer.fee)
|
||||
# Verify that we have enough ETH to send, and that after the transfer the balance will be either
|
||||
# exactly zero or at least MIN_DEPOSIT_AMOUNT
|
||||
assert (
|
||||
state.validator_balances[transfer.from] == transfer.amount + transfer.fee or
|
||||
state.validator_balances[transfer.from] >= transfer.amount + transfer.fee + MIN_DEPOSIT_AMOUNT
|
||||
)
|
||||
# A transfer is valid in only one slot
|
||||
assert state.slot == transfer.slot
|
||||
# Only withdrawn or not-yet-deposited accounts can transfer
|
||||
assert (
|
||||
get_current_epoch(state) >= state.validator_registry[transfer.from].withdrawable_epoch or
|
||||
state.validator_registry[transfer.from].activation_epoch == FAR_FUTURE_EPOCH
|
||||
)
|
||||
# Verify that the pubkey is valid
|
||||
assert (
|
||||
state.validator_registry[transfer.from].withdrawal_credentials ==
|
||||
BLS_WITHDRAWAL_PREFIX_BYTE + hash(transfer.pubkey)[1:]
|
||||
)
|
||||
# Verify that the signature is valid
|
||||
assert bls_verify(
|
||||
pubkey=transfer.pubkey,
|
||||
message_hash=signed_root(transfer, "signature"),
|
||||
signature=transfer.signature,
|
||||
domain=get_domain(state.fork, slot_to_epoch(transfer.slot), DOMAIN_TRANSFER)
|
||||
)
|
||||
# Process the transfer
|
||||
state.validator_balances[transfer.from] -= transfer.amount + transfer.fee
|
||||
state.validator_balances[transfer.to] += transfer.amount
|
||||
state.validator_balances[get_beacon_proposer_index(state, state.slot)] += transfer.fee
|
||||
```
|
||||
|
||||
### Per-epoch processing
|
||||
|
||||
|
|
|
@ -37,8 +37,8 @@ At the current stage, Phase 1, while fundamentally feature-complete, is still su
|
|||
- [`BranchResponse`](#branchresponse)
|
||||
- [`BranchChallengeRecord`](#branchchallengerecord)
|
||||
- [`SubkeyReveal`](#subkeyreveal)
|
||||
- [Helpers](#helpers)
|
||||
- [`get_attestation_merkle_depth`](#get_attestation_merkle_depth)
|
||||
- [Helpers](#helpers)
|
||||
- [`get_attestation_data_merkle_depth`](#get_attestation_data_merkle_depth)
|
||||
- [`epoch_to_custody_period`](#epoch_to_custody_period)
|
||||
- [`slot_to_custody_period`](#slot_to_custody_period)
|
||||
- [`get_current_custody_period`](#get_current_custody_period)
|
||||
|
@ -74,6 +74,9 @@ Phase 1 depends upon all of the constants defined in [Phase 0](0_beacon-chain.md
|
|||
| `SHARD_CHUNK_SIZE` | 2**5 (= 32) | bytes |
|
||||
| `SHARD_BLOCK_SIZE` | 2**14 (= 16,384) | bytes |
|
||||
| `MINOR_REWARD_QUOTIENT` | 2**8 (= 256) | |
|
||||
| `MAX_POC_RESPONSE_DEPTH` | 5 | |
|
||||
| `ZERO_PUBKEY` | int_to_bytes48(0)| |
|
||||
| `VALIDATOR_NULL` | 2**64 - 1 | |
|
||||
|
||||
#### Time parameters
|
||||
|
||||
|
@ -87,19 +90,23 @@ Phase 1 depends upon all of the constants defined in [Phase 0](0_beacon-chain.md
|
|||
|
||||
#### Max operations per block
|
||||
|
||||
| Name | Value |
|
||||
|-------------------------------|---------------|
|
||||
| `MAX_BRANCH_CHALLENGES` | 2**2 (= 4) |
|
||||
| `MAX_BRANCH_RESPONSES` | 2**4 (= 16) |
|
||||
| `MAX_EARLY_SUBKEY_REVEALS` | 2**4 (= 16) |
|
||||
| Name | Value |
|
||||
|----------------------------------------------------|---------------|
|
||||
| `MAX_BRANCH_CHALLENGES` | 2**2 (= 4) |
|
||||
| `MAX_BRANCH_RESPONSES` | 2**4 (= 16) |
|
||||
| `MAX_EARLY_SUBKEY_REVEALS` | 2**4 (= 16) |
|
||||
| `MAX_INTERACTIVE_CUSTODY_CHALLENGE_INITIATIONS` | 2 |
|
||||
| `MAX_INTERACTIVE_CUSTODY_CHALLENGE_RESPONSES` | 16 |
|
||||
| `MAX_INTERACTIVE_CUSTODY_CHALLENGE_CONTINUTATIONS` | 16 |
|
||||
|
||||
#### Signature domains
|
||||
|
||||
| Name | Value |
|
||||
|------------------------|-----------------|
|
||||
| `DOMAIN_SHARD_PROPOSER`| 129 |
|
||||
| `DOMAIN_SHARD_ATTESTER`| 130 |
|
||||
| `DOMAIN_CUSTODY_SUBKEY`| 131 |
|
||||
| Name | Value |
|
||||
|------------------------------|-----------------|
|
||||
| `DOMAIN_SHARD_PROPOSER` | 129 |
|
||||
| `DOMAIN_SHARD_ATTESTER` | 130 |
|
||||
| `DOMAIN_CUSTODY_SUBKEY` | 131 |
|
||||
| `DOMAIN_CUSTODY_INTERACTIVE` | 132 |
|
||||
|
||||
# Shard chains and crosslink data
|
||||
|
||||
|
@ -163,7 +170,6 @@ def get_persistent_committee(state: BeaconState,
|
|||
[i for i in later_committee if epoch % PERSISTENT_COMMITTEE_PERIOD >= get_switchover_epoch(i)]
|
||||
)))
|
||||
```
|
||||
|
||||
#### `get_shard_proposer_index`
|
||||
|
||||
```python
|
||||
|
@ -295,7 +301,7 @@ The `shard_chain_commitment` is only valid if it equals `compute_commitment(head
|
|||
|
||||
### Shard block fork choice rule
|
||||
|
||||
The fork choice rule for any shard is LMD GHOST using the shard chain attestations of the persistent committee and the beacon chain attestations of the crosslink committee currently assigned to that shard, but instead of being rooted in the genesis it is rooted in the latest block referenced in the most recent accepted crosslink (ie. `state.crosslinks[shard].crosslink_data_root`). Only blocks whose `beacon_chain_ref` is the block in the main beacon chain at the specified `slot` should be considered (if the beacon chain skips a slot, then the block at that slot is considered to be the block in the beacon chain at the highest slot lower than a slot).
|
||||
The fork choice rule for any shard is LMD GHOST using the shard chain attestations of the persistent committee and the beacon chain attestations of the crosslink committee currently assigned to that shard, but instead of being rooted in the genesis it is rooted in the block referenced in the most recent accepted crosslink (ie. `state.crosslinks[shard].shard_block_root`). Only blocks whose `beacon_chain_ref` is the block in the main beacon chain at the specified `slot` should be considered (if the beacon chain skips a slot, then the block at that slot is considered to be the block in the beacon chain at the highest slot lower than a slot).
|
||||
|
||||
# Updates to the beacon chain
|
||||
|
||||
|
@ -306,7 +312,6 @@ The fork choice rule for any shard is LMD GHOST using the shard chain attestatio
|
|||
Add member values to the end of the `Validator` object:
|
||||
|
||||
```python
|
||||
'open_branch_challenges': [BranchChallengeRecord],
|
||||
'next_subkey_to_reveal': 'uint64',
|
||||
'reveal_max_periods_late': 'uint64',
|
||||
```
|
||||
|
@ -314,7 +319,6 @@ Add member values to the end of the `Validator` object:
|
|||
And the initializers:
|
||||
|
||||
```python
|
||||
'open_branch_challenges': [],
|
||||
'next_subkey_to_reveal': get_current_custody_period(state),
|
||||
'reveal_max_periods_late': 0,
|
||||
```
|
||||
|
@ -327,6 +331,10 @@ Add member values to the `BeaconBlockBody` structure:
|
|||
'branch_challenges': [BranchChallenge],
|
||||
'branch_responses': [BranchResponse],
|
||||
'subkey_reveals': [SubkeyReveal],
|
||||
'interactive_custody_challenge_initiations': [InteractiveCustodyChallengeInitiation],
|
||||
'interactive_custody_challenge_responses': [InteractiveCustodyChallengeResponse],
|
||||
'interactive_custody_challenge_continuations': [InteractiveCustodyChallengeContinuation],
|
||||
|
||||
```
|
||||
|
||||
And initialize to the following:
|
||||
|
@ -337,6 +345,17 @@ And initialize to the following:
|
|||
'subkey_reveals': [],
|
||||
```
|
||||
|
||||
### `BeaconState`
|
||||
|
||||
Add member values to the `BeaconState` structure:
|
||||
|
||||
```python
|
||||
'branch_challenge_records': [BranchChallengeRecord],
|
||||
'next_branch_challenge_id': 'uint64',
|
||||
'custody_challenge_records': [InteractiveCustodyChallengeRecord],
|
||||
'next_custody_challenge_id': 'uint64',
|
||||
```
|
||||
|
||||
### `BranchChallenge`
|
||||
|
||||
Define a `BranchChallenge` as follows:
|
||||
|
@ -355,11 +374,10 @@ Define a `BranchResponse` as follows:
|
|||
|
||||
```python
|
||||
{
|
||||
'responder_index': 'uint64',
|
||||
'challenge_id': 'uint64',
|
||||
'responding_to_custody_challenge': 'bool',
|
||||
'data': 'bytes32',
|
||||
'branch': ['bytes32'],
|
||||
'data_index': 'uint64',
|
||||
'root': 'bytes32',
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -369,14 +387,75 @@ Define a `BranchChallengeRecord` as follows:
|
|||
|
||||
```python
|
||||
{
|
||||
'challenge_id': 'uint64',
|
||||
'challenger_index': 'uint64',
|
||||
'responder_index': 'uint64',
|
||||
'root': 'bytes32',
|
||||
'depth': 'uint64',
|
||||
'inclusion_epoch': 'uint64',
|
||||
'deadline': 'uint64',
|
||||
'data_index': 'uint64',
|
||||
}
|
||||
```
|
||||
|
||||
### `InteractiveCustodyChallengeRecord`
|
||||
|
||||
```python
|
||||
{
|
||||
'challenge_id': 'uint64',
|
||||
'challenger_index': 'uint64',
|
||||
'responder_index': 'uint64',
|
||||
# Initial data root
|
||||
'data_root': 'bytes32',
|
||||
# Initial custody bit
|
||||
'custody_bit': 'bool',
|
||||
# Responder subkey
|
||||
'responder_subkey': 'bytes96',
|
||||
# The hash in the PoC tree in the position that we are currently at
|
||||
'current_custody_tree_node': 'bytes32',
|
||||
# The position in the tree, in terms of depth and position offset
|
||||
'depth': 'uint64',
|
||||
'offset': 'uint64',
|
||||
# Max depth of the branch
|
||||
'max_depth': 'uint64',
|
||||
# Deadline to respond (as an epoch)
|
||||
'deadline': 'uint64',
|
||||
}
|
||||
```
|
||||
|
||||
### `InteractiveCustodyChallengeInitiation`
|
||||
|
||||
```python
|
||||
{
|
||||
'attestation': SlashableAttestation,
|
||||
'responder_index': 'uint64',
|
||||
'challenger_index': 'uint64',
|
||||
'responder_subkey': 'bytes96',
|
||||
'signature': 'bytes96',
|
||||
}
|
||||
```
|
||||
|
||||
### `InteractiveCustodyChallengeResponse`
|
||||
|
||||
```python
|
||||
{
|
||||
'challenge_id': 'uint64',
|
||||
'hashes': ['bytes32'],
|
||||
'signature': 'bytes96',
|
||||
}
|
||||
```
|
||||
|
||||
### `InteractiveCustodyChallengeContinuation`
|
||||
|
||||
```python
|
||||
{
|
||||
'challenge_id': 'uint64',
|
||||
'sub_index': 'uint64',
|
||||
'new_custody_tree_node': 'bytes32',
|
||||
'proof': ['bytes32'],
|
||||
'signature': 'bytes96',
|
||||
}
|
||||
```
|
||||
|
||||
### `SubkeyReveal`
|
||||
|
||||
Define a `SubkeyReveal` as follows:
|
||||
|
@ -393,6 +472,20 @@ Define a `SubkeyReveal` as follows:
|
|||
|
||||
## Helpers
|
||||
|
||||
### `get_branch_challenge_record_by_id`
|
||||
|
||||
```python
|
||||
def get_branch_challenge_record_by_id(state: BeaconState, id: int) -> BranchChallengeRecord:
|
||||
return [c for c in state.branch_challenges if c.challenge_id == id][0]
|
||||
```
|
||||
|
||||
### `get_custody_challenge_record_by_id`
|
||||
|
||||
```python
|
||||
def get_custody_challenge_record_by_id(state: BeaconState, id: int) -> BranchChallengeRecord:
|
||||
return [c for c in state.branch_challenges if c.challenge_id == id][0]
|
||||
```
|
||||
|
||||
### `get_attestation_merkle_depth`
|
||||
|
||||
```python
|
||||
|
@ -458,6 +551,19 @@ def verify_custody_subkey_reveal(pubkey: bytes48,
|
|||
)
|
||||
```
|
||||
|
||||
### `verify_signed_challenge_message`
|
||||
|
||||
```python
|
||||
def verify_signed_challenge_message(message: Any, pubkey: bytes48) -> bool:
|
||||
return bls_verify(
|
||||
message_hash=signed_root(message, 'signature'),
|
||||
pubkey=pubkey,
|
||||
signature=message.signature,
|
||||
domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_INTERACTIVE)
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### `penalize_validator`
|
||||
|
||||
Change the definition of `penalize_validator` as follows:
|
||||
|
@ -498,29 +604,88 @@ Add the following operations to the per-slot processing, in order the given belo
|
|||
|
||||
Verify that `len(block.body.branch_challenges) <= MAX_BRANCH_CHALLENGES`.
|
||||
|
||||
For each `challenge` in `block.body.branch_challenges`:
|
||||
For each `challenge` in `block.body.branch_challenges`, run:
|
||||
|
||||
* Verify that `slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_BRANCH_CHALLENGE_DELAY`.
|
||||
* Verify that `state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_BRANCH_CHALLENGE_DELAY`.
|
||||
* Verify that `verify_slashable_attestation(state, challenge.attestation)` returns `True`.
|
||||
* Verify that `challenge.responder_index` is in `challenge.attestation.validator_indices`.
|
||||
* Let `depth = get_attestation_merkle_depth(challenge.attestation)`. Verify that `challenge.data_index < 2**depth`.
|
||||
* Verify that there does not exist a `BranchChallengeRecord` in `state.validator_registry[challenge.responder_index].open_branch_challenges` with `root == challenge.attestation.data.shard_chain_commitment` and `data_index == data_index`.
|
||||
* Append to `state.validator_registry[challenge.responder_index].open_branch_challenges` the object `BranchChallengeRecord(challenger_index=get_beacon_proposer_index(state, state.slot), root=challenge.attestation.data.shard_chain_commitment, depth=depth, inclusion_epoch=get_current_epoch(state), data_index=data_index)`.
|
||||
|
||||
**Invariant**: the `open_branch_challenges` array will always stay sorted in order of `inclusion_epoch`.
|
||||
```python
|
||||
def process_branch_challenge(challenge: BranchChallenge,
|
||||
state: BeaconState):
|
||||
# Check that it's not too late to challenge
|
||||
assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_BRANCH_CHALLENGE_DELAY
|
||||
assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_BRANCH_CHALLENGE_DELAY
|
||||
# Check the attestation is valid
|
||||
assert verify_slashable_attestation(state, challenge.attestation)
|
||||
# Check that the responder participated
|
||||
assert challenger.responder_index in challenge.attestation.validator_indices
|
||||
# Check the challenge is not a duplicate
|
||||
assert [
|
||||
c for c in state.branch_challenge_records if c.root == challenge.attestation.data.crosslink_data_root and
|
||||
c.data_index == challenge.data_index
|
||||
] == []
|
||||
# Check validity of depth
|
||||
depth = get_attestation_merkle_depth(challenge.attestation)
|
||||
assert c.data_index < 2**depth
|
||||
# Add new challenge
|
||||
state.branch_challenge_records.append(BranchChallengeRecord(
|
||||
challenge_id=state.next_branch_challenge_id,
|
||||
challenger_index=get_beacon_proposer_index(state, state.slot),
|
||||
root=challenge.attestation.data.shard_chain_commitment,
|
||||
depth=depth,
|
||||
deadline=get_current_epoch(state) + CHALLENGE_RESPONSE_DEADLINE,
|
||||
data_index=challenge.data_index
|
||||
))
|
||||
state.next_branch_challenge_id += 1
|
||||
```
|
||||
|
||||
#### Branch responses
|
||||
|
||||
Verify that `len(block.body.branch_responses) <= MAX_BRANCH_RESPONSES`.
|
||||
|
||||
For each `response` in `block.body.branch_responses`:
|
||||
For each `response` in `block.body.branch_responses`, if `response.responding_to_custody_challenge == False`, run:
|
||||
|
||||
* Find the `BranchChallengeRecord` in `state.validator_registry[response.responder_index].open_branch_challenges` whose (`root`, `data_index`) match the (`root`, `data_index`) of the `response`. Verify that one such record exists (it is not possible for there to be more than one), call it `record`.
|
||||
* Verify that `verify_merkle_branch(leaf=response.data, branch=response.branch, depth=record.depth, index=record.data_index, root=record.root)` is True.
|
||||
* Verify that `get_current_epoch(state) >= record.inclusion_epoch + ENTRY_EXIT_DELAY`.
|
||||
* Remove the `record` from `state.validator_registry[response.responder_index].open_branch_challenges`
|
||||
* Determine the proposer `proposer_index = get_beacon_proposer_index(state, state.slot)` and set `state.validator_balances[proposer_index] += base_reward(state, index) // MINOR_REWARD_QUOTIENT`.
|
||||
```python
|
||||
def process_branch_exploration_response(response: BranchResponse,
|
||||
state: BeaconState):
|
||||
challenge = get_branch_challenge_record_by_id(response.challenge_id)
|
||||
assert verify_merkle_branch(
|
||||
leaf=response.data,
|
||||
branch=response.branch,
|
||||
depth=challenge.depth,
|
||||
index=challenge.data_index,
|
||||
root=challenge.root
|
||||
)
|
||||
# Must wait at least ENTRY_EXIT_DELAY before responding to a branch challenge
|
||||
assert get_current_epoch(state) >= challenge.inclusion_epoch + ENTRY_EXIT_DELAY
|
||||
state.branch_challenge_records.pop(challenge)
|
||||
# Reward the proposer
|
||||
proposer_index = get_beacon_proposer_index(state, state.slot)
|
||||
state.validator_balances[proposer_index] += base_reward(state, index) // MINOR_REWARD_QUOTIENT
|
||||
```
|
||||
|
||||
If `response.responding_to_custody_challenge == True`, run:
|
||||
|
||||
```python
|
||||
def process_branch_custody_response(response: BranchResponse,
|
||||
state: BeaconState):
|
||||
challenge = get_custody_challenge_record_by_id(response.challenge_id)
|
||||
responder = state.validator_registry[challenge.responder_index]
|
||||
# Verify we're not too late
|
||||
assert get_current_epoch(state) < responder.withdrawable_epoch
|
||||
# Verify the Merkle branch *of the data tree*
|
||||
assert verify_merkle_branch(
|
||||
leaf=response.data,
|
||||
branch=response.branch,
|
||||
depth=challenge.max_depth,
|
||||
index=challenge.offset,
|
||||
root=challenge.data_root
|
||||
)
|
||||
# Responder wins
|
||||
if hash(challenge.responder_subkey + response.data) == challenge.current_custody_tree_node:
|
||||
penalize_validator(state, challenge.challenger_index, challenge.responder_index)
|
||||
# Challenger wins
|
||||
else:
|
||||
penalize_validator(state, challenge.responder_index, challenge.challenger_index)
|
||||
state.custody_challenge_records.pop(challenge)
|
||||
```
|
||||
|
||||
#### Subkey reveals
|
||||
|
||||
|
@ -546,6 +711,126 @@ In case (ii):
|
|||
* Set `state.validator_registry[reveal.validator_index].next_subkey_to_reveal += 1`
|
||||
* Set `state.validator_registry[reveal.validator_index].reveal_max_periods_late = max(state.validator_registry[reveal.validator_index].reveal_max_periods_late, get_current_period(state) - reveal.period)`.
|
||||
|
||||
#### Interactive custody challenge initiations
|
||||
|
||||
Verify that `len(block.body.interactive_custody_challenge_initiations) <= MAX_INTERACTIVE_CUSTODY_CHALLENGE_INITIATIONS`.
|
||||
|
||||
For each `initiation` in `block.body.interactive_custody_challenge_initiations`, use the following function to process it:
|
||||
|
||||
```python
|
||||
def process_initiation(initiation: InteractiveCustodyChallengeInitiation,
|
||||
state: BeaconState):
|
||||
challenger = state.validator_registry[initiation.challenger_index]
|
||||
responder = state.validator_registry[initiation.responder_index]
|
||||
# Verify the signature
|
||||
assert verify_signed_challenge_message(initiation, challenger.pubkey)
|
||||
# Verify the attestation
|
||||
assert verify_slashable_attestation(initiation.attestation, state)
|
||||
# Check that the responder actually participated in the attestation
|
||||
assert initiation.responder_index in attestation.validator_indices
|
||||
# Any validator can be a challenger or responder of max 1 challenge at a time
|
||||
for c in state.custody_challenge_records:
|
||||
assert c.challenger_index != initiation.challenger_index
|
||||
assert c.responder_index != initiation.responder_index
|
||||
# Can't challenge if you've been penalized
|
||||
assert challenger.penalized_epoch == FAR_FUTURE_EPOCH
|
||||
# Make sure the revealed subkey is valid
|
||||
assert verify_custody_subkey_reveal(
|
||||
pubkey=state.validator_registry[responder_index].pubkey,
|
||||
subkey=initiation.responder_subkey,
|
||||
period=slot_to_custody_period(attestation.data.slot)
|
||||
)
|
||||
# Verify that the attestation is still eligible for challenging
|
||||
min_challengeable_epoch = responder.exit_epoch - CUSTODY_PERIOD_LENGTH * (1 + responder.reveal_max_periods_late)
|
||||
assert min_challengeable_epoch <= slot_to_epoch(initiation.attestation.data.slot)
|
||||
# Create a new challenge object
|
||||
state.branch_challenge_records.append(InteractiveCustodyChallengeRecord(
|
||||
challenge_id=state.next_branch_challenge_id,
|
||||
challenger_index=initiation.challenger_index,
|
||||
responder_index=initiation.responder_index,
|
||||
data_root=attestation.custody_commitment,
|
||||
custody_bit=get_bitfield_bit(attestation.custody_bitfield, attestation.validator_indices.index(responder_index)),
|
||||
responder_subkey=responder_subkey,
|
||||
current_custody_tree_node=ZERO_HASH,
|
||||
depth=0,
|
||||
offset=0,
|
||||
max_depth=get_attestation_data_merkle_depth(initiation.attestation.data),
|
||||
deadline=get_current_epoch(state) + CHALLENGE_RESPONSE_DEADLINE
|
||||
))
|
||||
state.next_branch_challenge_id += 1
|
||||
# Responder can't withdraw yet!
|
||||
state.validator_registry[responder_index].withdrawable_epoch = FAR_FUTURE_EPOCH
|
||||
```
|
||||
|
||||
#### Interactive custody challenge responses
|
||||
|
||||
A response provides 32 hashes that are under current known proof of custody tree node. Note that at the beginning the tree node is just one bit of the custody root, so we ask the responder to sign to commit to the top 5 levels of the tree and therefore the root hash; at all other stages in the game responses are self-verifying.
|
||||
|
||||
Verify that `len(block.body.interactive_custody_challenge_responses) <= MAX_INTERACTIVE_CUSTODY_CHALLENGE_RESPONSES`.
|
||||
|
||||
For each `response` in `block.body.interactive_custody_challenge_responses`, use the following function to process it:
|
||||
|
||||
```python
|
||||
def process_response(response: InteractiveCustodyChallengeResponse,
|
||||
state: State):
|
||||
challenge = get_custody_challenge_record_by_id(state, response.challenge_id)
|
||||
responder = state.validator_registry[challenge.responder_index]
|
||||
# Check that the right number of hashes was provided
|
||||
expected_depth = min(challenge.max_depth - challenge.depth, MAX_POC_RESPONSE_DEPTH)
|
||||
assert 2**expected_depth == len(response.hashes)
|
||||
# Must make some progress!
|
||||
assert expected_depth > 0
|
||||
# Check the hashes match the previously provided root
|
||||
root = merkle_root(response.hashes)
|
||||
# If this is the first response check the bit and the signature and set the root
|
||||
if challenge.depth == 0:
|
||||
assert get_bitfield_bit(root, 0) == challenge.custody_bit
|
||||
assert verify_signed_challenge_message(response, responder.pubkey)
|
||||
challenge.current_custody_tree_node = root
|
||||
# Otherwise just check the response against the root
|
||||
else:
|
||||
assert root == challenge_data.current_custody_tree_node
|
||||
# Update challenge data
|
||||
challenge.deadline=FAR_FUTURE_EPOCH
|
||||
responder.withdrawable_epoch = get_current_epoch(state) + MAX_POC_RESPONSE_DEPTH
|
||||
```
|
||||
|
||||
#### Interactive custody challenge continuations
|
||||
|
||||
Once a response provides 32 hashes, the challenger has the right to choose any one of them that they feel is constructed incorrectly to continue the game. Note that eventually, the game will get to the point where the `new_custody_tree_node` is a leaf node.
|
||||
|
||||
Verify that `len(block.body.interactive_custody_challenge_continuations) <= MAX_INTERACTIVE_CUSTODY_CHALLENGE_CONTINUATIONS`.
|
||||
|
||||
For each `continuation` in `block.body.interactive_custody_challenge_continuations`, use the following function to process it:
|
||||
|
||||
```python
|
||||
def process_continuation(continuation: InteractiveCustodyChallengeContinuation,
|
||||
state: State):
|
||||
challenge = get_custody_challenge_record_by_id(state, continuation.challenge_id)
|
||||
challenger = state.validator_registry[challenge.challenger_index]
|
||||
responder = state.validator_registry[challenge.responder_index]
|
||||
expected_depth = min(challenge_data.max_depth - challenge_data.depth, MAX_POC_RESPONSE_DEPTH)
|
||||
# Verify we're not too late
|
||||
assert get_current_epoch(state) < responder.withdrawable_epoch
|
||||
# Verify the Merkle branch (the previous custody response provided the next level of hashes so the
|
||||
# challenger has the info to make any Merkle branch)
|
||||
assert verify_merkle_branch(
|
||||
leaf=new_custody_tree_node,
|
||||
branch=continuation.proof,
|
||||
depth=expected_depth,
|
||||
index=sub_index,
|
||||
root=challenge_data.current_custody_tree_node
|
||||
)
|
||||
# Verify signature
|
||||
assert verify_signed_challenge_message(continuation, challenger.pubkey)
|
||||
# Update the challenge data
|
||||
challenge.current_custody_tree_node = continuation.new_custody_tree_node
|
||||
challenge.depth += expected_depth
|
||||
challenge.deadline = get_current_epoch(state) + MAX_POC_RESPONSE_DEPTH
|
||||
responder.withdrawable_epoch = FAR_FUTURE_EPOCH
|
||||
challenge.offset = challenge_data.offset * 2**expected_depth + sub_index
|
||||
```
|
||||
|
||||
## Per-epoch processing
|
||||
|
||||
Add the following loop immediately below the `process_ejections` loop:
|
||||
|
@ -553,12 +838,18 @@ Add the following loop immediately below the `process_ejections` loop:
|
|||
```python
|
||||
def process_challenge_absences(state: BeaconState) -> None:
|
||||
"""
|
||||
Iterate through the validator registry
|
||||
Iterate through the challenge list
|
||||
and penalize validators with balance that did not answer challenges.
|
||||
"""
|
||||
for index, validator in enumerate(state.validator_registry):
|
||||
if len(validator.open_branch_challenges) > 0 and get_current_epoch(state) > validator.open_branch_challenges[0].inclusion_epoch + CHALLENGE_RESPONSE_DEADLINE:
|
||||
penalize_validator(state, index, validator.open_branch_challenges[0].challenger_index)
|
||||
for c in state.branch_challenge_records:
|
||||
if get_current_epoch(state) > c.deadline:
|
||||
penalize_validator(state, c.responder_index, c.challenger_index)
|
||||
|
||||
for c in state.custody_challenge_records:
|
||||
if get_current_epoch(state) > c.deadline:
|
||||
penalize_validator(state, c.responder_index, c.challenger_index)
|
||||
if get_current_epoch(state) > state.validator_registry[c.responder_index].withdrawable_epoch:
|
||||
penalize_validator(state, c.challenger_index, c.responder_index)
|
||||
```
|
||||
|
||||
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):
|
||||
|
@ -567,7 +858,7 @@ In `process_penalties_and_exits`, change the definition of `eligible` to the fol
|
|||
def eligible(index):
|
||||
validator = state.validator_registry[index]
|
||||
# Cannot exit if there are still open branch challenges
|
||||
if len(validator.open_branch_challenges) > 0:
|
||||
if [c for c in state.branch_challenge_records if c.responder_index == index] != []:
|
||||
return False
|
||||
# Cannot exit if you have not revealed all of your subkeys
|
||||
elif validator.next_subkey_to_reveal <= epoch_to_custody_period(validator.exit_epoch):
|
||||
|
@ -587,7 +878,15 @@ Run the following on the fork block after per-slot processing and before per-blo
|
|||
For all `validator` in `ValidatorRegistry`, update it to the new format and fill the new member values with:
|
||||
|
||||
```python
|
||||
'open_branch_challenges': [],
|
||||
'next_subkey_to_reveal': get_current_custody_period(state),
|
||||
'reveal_max_periods_late': 0,
|
||||
```
|
||||
|
||||
Update the `BeaconState` to the new format and fill the new member values with:
|
||||
|
||||
```python
|
||||
'branch_challenge_records': [],
|
||||
'next_branch_challenge_id': 0,
|
||||
'custody_challenge_records': [],
|
||||
'next_custody_challenge_id': 0,
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue