Move state trans fns into state_processing
This commit is contained in:
parent
7503f31ddc
commit
1028acf3f1
22
eth2/state_processing/src/common/exit_validator.rs
Normal file
22
eth2/state_processing/src/common/exit_validator.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use types::{BeaconStateError as Error, *};
|
||||||
|
|
||||||
|
/// Exit the validator of the given `index`.
|
||||||
|
///
|
||||||
|
/// Spec v0.5.0
|
||||||
|
pub fn exit_validator(
|
||||||
|
state: &mut BeaconState,
|
||||||
|
validator_index: usize,
|
||||||
|
spec: &ChainSpec,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if validator_index >= state.validator_registry.len() {
|
||||||
|
return Err(Error::UnknownValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
let delayed_epoch = state.get_delayed_activation_exit_epoch(state.current_epoch(spec), spec);
|
||||||
|
|
||||||
|
if state.validator_registry[validator_index].exit_epoch > delayed_epoch {
|
||||||
|
state.validator_registry[validator_index].exit_epoch = delayed_epoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
7
eth2/state_processing/src/common/mod.rs
Normal file
7
eth2/state_processing/src/common/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
mod exit_validator;
|
||||||
|
mod slash_validator;
|
||||||
|
mod verify_bitfield;
|
||||||
|
|
||||||
|
pub use exit_validator::exit_validator;
|
||||||
|
pub use slash_validator::slash_validator;
|
||||||
|
pub use verify_bitfield::verify_bitfield_length;
|
62
eth2/state_processing/src/common/slash_validator.rs
Normal file
62
eth2/state_processing/src/common/slash_validator.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use crate::common::exit_validator;
|
||||||
|
use types::{BeaconStateError as Error, *};
|
||||||
|
|
||||||
|
/// Slash the validator with index ``index``.
|
||||||
|
///
|
||||||
|
/// Spec v0.5.0
|
||||||
|
pub fn slash_validator(
|
||||||
|
state: &mut BeaconState,
|
||||||
|
validator_index: usize,
|
||||||
|
spec: &ChainSpec,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let current_epoch = state.current_epoch(spec);
|
||||||
|
|
||||||
|
if (validator_index >= state.validator_registry.len())
|
||||||
|
| (validator_index >= state.validator_balances.len())
|
||||||
|
{
|
||||||
|
return Err(BeaconStateError::UnknownValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
let validator = &state.validator_registry[validator_index];
|
||||||
|
|
||||||
|
let effective_balance = state.get_effective_balance(validator_index, spec)?;
|
||||||
|
|
||||||
|
// A validator that is withdrawn cannot be slashed.
|
||||||
|
//
|
||||||
|
// This constraint will be lifted in Phase 0.
|
||||||
|
if state.slot
|
||||||
|
>= validator
|
||||||
|
.withdrawable_epoch
|
||||||
|
.start_slot(spec.slots_per_epoch)
|
||||||
|
{
|
||||||
|
return Err(Error::ValidatorIsWithdrawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_validator(state, validator_index, spec)?;
|
||||||
|
|
||||||
|
state.set_slashed_balance(
|
||||||
|
current_epoch,
|
||||||
|
state.get_slashed_balance(current_epoch, spec)? + effective_balance,
|
||||||
|
spec,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let whistleblower_index =
|
||||||
|
state.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)?;
|
||||||
|
let whistleblower_reward = effective_balance / spec.whistleblower_reward_quotient;
|
||||||
|
|
||||||
|
safe_add_assign!(
|
||||||
|
state.validator_balances[whistleblower_index as usize],
|
||||||
|
whistleblower_reward
|
||||||
|
);
|
||||||
|
safe_sub_assign!(
|
||||||
|
state.validator_balances[validator_index],
|
||||||
|
whistleblower_reward
|
||||||
|
);
|
||||||
|
|
||||||
|
state.validator_registry[validator_index].slashed = true;
|
||||||
|
|
||||||
|
state.validator_registry[validator_index].withdrawable_epoch =
|
||||||
|
current_epoch + Epoch::from(spec.latest_slashed_exit_length);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
use crate::*;
|
use types::*;
|
||||||
|
|
||||||
/// Verify ``bitfield`` against the ``committee_size``.
|
/// Verify ``bitfield`` against the ``committee_size``.
|
||||||
///
|
///
|
@ -37,8 +37,7 @@ pub fn get_genesis_state(
|
|||||||
.get_active_validator_indices(spec.genesis_epoch, spec)?
|
.get_active_validator_indices(spec.genesis_epoch, spec)?
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let genesis_active_index_root = Hash256::from_slice(&active_validator_indices.hash_tree_root());
|
let genesis_active_index_root = Hash256::from_slice(&active_validator_indices.hash_tree_root());
|
||||||
state.latest_active_index_roots =
|
state.fill_active_index_roots_with(genesis_active_index_root, spec);
|
||||||
vec![genesis_active_index_root; spec.latest_active_index_roots_length as usize];
|
|
||||||
|
|
||||||
// Generate the current shuffling seed.
|
// Generate the current shuffling seed.
|
||||||
state.current_shuffling_seed = state.generate_seed(spec.genesis_epoch, spec)?;
|
state.current_shuffling_seed = state.generate_seed(spec.genesis_epoch, spec)?;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
|
pub mod common;
|
||||||
pub mod get_genesis_state;
|
pub mod get_genesis_state;
|
||||||
pub mod per_block_processing;
|
pub mod per_block_processing;
|
||||||
pub mod per_epoch_processing;
|
pub mod per_epoch_processing;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use self::verify_proposer_slashing::verify_proposer_slashing;
|
use self::verify_proposer_slashing::verify_proposer_slashing;
|
||||||
|
use crate::common::slash_validator;
|
||||||
use errors::{BlockInvalid as Invalid, BlockProcessingError as Error, IntoWithIndex};
|
use errors::{BlockInvalid as Invalid, BlockProcessingError as Error, IntoWithIndex};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use ssz::{SignedRoot, TreeHash};
|
use ssz::{SignedRoot, TreeHash};
|
||||||
@ -222,7 +223,7 @@ pub fn process_proposer_slashings(
|
|||||||
|
|
||||||
// Update the state.
|
// Update the state.
|
||||||
for proposer_slashing in proposer_slashings {
|
for proposer_slashing in proposer_slashings {
|
||||||
state.slash_validator(proposer_slashing.proposer_index as usize, spec)?;
|
slash_validator(state, proposer_slashing.proposer_index as usize, spec)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -279,7 +280,7 @@ pub fn process_attester_slashings(
|
|||||||
.map_err(|e| e.into_with_index(i))?;
|
.map_err(|e| e.into_with_index(i))?;
|
||||||
|
|
||||||
for i in slashable_indices {
|
for i in slashable_indices {
|
||||||
state.slash_validator(i as usize, spec)?;
|
slash_validator(state, i as usize, spec)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::errors::{AttestationInvalid as Invalid, AttestationValidationError as Error};
|
use super::errors::{AttestationInvalid as Invalid, AttestationValidationError as Error};
|
||||||
|
use crate::common::verify_bitfield_length;
|
||||||
use ssz::TreeHash;
|
use ssz::TreeHash;
|
||||||
use types::beacon_state::helpers::*;
|
|
||||||
use types::*;
|
use types::*;
|
||||||
|
|
||||||
/// Indicates if an `Attestation` is valid to be included in a block in the current epoch of the
|
/// Indicates if an `Attestation` is valid to be included in a block in the current epoch of the
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use super::errors::{
|
use super::errors::{
|
||||||
SlashableAttestationInvalid as Invalid, SlashableAttestationValidationError as Error,
|
SlashableAttestationInvalid as Invalid, SlashableAttestationValidationError as Error,
|
||||||
};
|
};
|
||||||
|
use crate::common::verify_bitfield_length;
|
||||||
use ssz::TreeHash;
|
use ssz::TreeHash;
|
||||||
use types::beacon_state::helpers::verify_bitfield_length;
|
|
||||||
use types::*;
|
use types::*;
|
||||||
|
|
||||||
/// Indicates if a `SlashableAttestation` is valid to be included in a block in the current epoch of the given
|
/// Indicates if a `SlashableAttestation` is valid to be included in a block in the current epoch of the given
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use errors::EpochProcessingError as Error;
|
use errors::EpochProcessingError as Error;
|
||||||
use integer_sqrt::IntegerSquareRoot;
|
use integer_sqrt::IntegerSquareRoot;
|
||||||
|
use process_ejections::process_ejections;
|
||||||
|
use process_exit_queue::process_exit_queue;
|
||||||
|
use process_slashings::process_slashings;
|
||||||
use process_validator_registry::process_validator_registry;
|
use process_validator_registry::process_validator_registry;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use ssz::TreeHash;
|
use ssz::TreeHash;
|
||||||
@ -11,8 +14,12 @@ use winning_root::{winning_root, WinningRoot};
|
|||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod get_attestation_participants;
|
pub mod get_attestation_participants;
|
||||||
pub mod inclusion_distance;
|
pub mod inclusion_distance;
|
||||||
|
pub mod process_ejections;
|
||||||
|
pub mod process_exit_queue;
|
||||||
|
pub mod process_slashings;
|
||||||
pub mod process_validator_registry;
|
pub mod process_validator_registry;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
pub mod update_validator_registry;
|
||||||
pub mod validator_statuses;
|
pub mod validator_statuses;
|
||||||
pub mod winning_root;
|
pub mod winning_root;
|
||||||
|
|
||||||
@ -45,14 +52,16 @@ pub fn per_epoch_processing(state: &mut BeaconState, spec: &ChainSpec) -> Result
|
|||||||
process_rewards_and_penalities(state, &mut statuses, &winning_root_for_shards, spec)?;
|
process_rewards_and_penalities(state, &mut statuses, &winning_root_for_shards, spec)?;
|
||||||
|
|
||||||
// Ejections
|
// Ejections
|
||||||
state.process_ejections(spec)?;
|
process_ejections(state, spec)?;
|
||||||
|
|
||||||
// Validator Registry
|
// Validator Registry
|
||||||
process_validator_registry(state, spec)?;
|
process_validator_registry(state, spec)?;
|
||||||
|
process_slashings(state, spec)?;
|
||||||
|
process_exit_queue(state, spec);
|
||||||
|
|
||||||
// Final updates
|
// Final updates
|
||||||
update_active_tree_index_roots(state, spec)?;
|
update_active_tree_index_roots(state, spec)?;
|
||||||
update_latest_slashed_balances(state, spec);
|
update_latest_slashed_balances(state, spec)?;
|
||||||
clean_attestations(state);
|
clean_attestations(state);
|
||||||
|
|
||||||
// Rotate the epoch caches to suit the epoch transition.
|
// Rotate the epoch caches to suit the epoch transition.
|
||||||
@ -451,9 +460,7 @@ pub fn update_active_tree_index_roots(
|
|||||||
)
|
)
|
||||||
.hash_tree_root();
|
.hash_tree_root();
|
||||||
|
|
||||||
state.latest_active_index_roots[(next_epoch.as_usize()
|
state.set_active_index_root(next_epoch, Hash256::from_slice(&active_tree_root[..]), spec)?;
|
||||||
+ spec.activation_exit_delay as usize)
|
|
||||||
% spec.latest_active_index_roots_length] = Hash256::from_slice(&active_tree_root[..]);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -461,12 +468,20 @@ pub fn update_active_tree_index_roots(
|
|||||||
/// Advances the state's `latest_slashed_balances` field.
|
/// Advances the state's `latest_slashed_balances` field.
|
||||||
///
|
///
|
||||||
/// Spec v0.4.0
|
/// Spec v0.4.0
|
||||||
pub fn update_latest_slashed_balances(state: &mut BeaconState, spec: &ChainSpec) {
|
pub fn update_latest_slashed_balances(
|
||||||
|
state: &mut BeaconState,
|
||||||
|
spec: &ChainSpec,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let current_epoch = state.current_epoch(spec);
|
let current_epoch = state.current_epoch(spec);
|
||||||
let next_epoch = state.next_epoch(spec);
|
let next_epoch = state.next_epoch(spec);
|
||||||
|
|
||||||
state.latest_slashed_balances[next_epoch.as_usize() % spec.latest_slashed_exit_length] =
|
state.set_slashed_balance(
|
||||||
state.latest_slashed_balances[current_epoch.as_usize() % spec.latest_slashed_exit_length];
|
next_epoch,
|
||||||
|
state.get_slashed_balance(current_epoch, spec)?,
|
||||||
|
spec,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes all pending attestations from the previous epoch.
|
/// Removes all pending attestations from the previous epoch.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use types::{beacon_state::helpers::verify_bitfield_length, *};
|
use crate::common::verify_bitfield_length;
|
||||||
|
use types::*;
|
||||||
|
|
||||||
/// Returns validator indices which participated in the attestation.
|
/// Returns validator indices which participated in the attestation.
|
||||||
///
|
///
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
use crate::common::exit_validator;
|
||||||
|
use types::{BeaconStateError as Error, *};
|
||||||
|
|
||||||
|
/// Iterate through the validator registry and eject active validators with balance below
|
||||||
|
/// ``EJECTION_BALANCE``.
|
||||||
|
///
|
||||||
|
/// Spec v0.5.0
|
||||||
|
pub fn process_ejections(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), Error> {
|
||||||
|
// There is an awkward double (triple?) loop here because we can't loop across the borrowed
|
||||||
|
// active validator indices and mutate state in the one loop.
|
||||||
|
let exitable: Vec<usize> = state
|
||||||
|
.get_active_validator_indices(state.current_epoch(spec), spec)?
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&i| {
|
||||||
|
if state.validator_balances[i as usize] < spec.ejection_balance {
|
||||||
|
Some(i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for validator_index in exitable {
|
||||||
|
exit_validator(state, validator_index, spec)?
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
use types::*;
|
||||||
|
|
||||||
|
/// Process the exit queue.
|
||||||
|
///
|
||||||
|
/// Spec v0.5.0
|
||||||
|
pub fn process_exit_queue(state: &mut BeaconState, spec: &ChainSpec) {
|
||||||
|
let current_epoch = state.current_epoch(spec);
|
||||||
|
|
||||||
|
let eligible = |index: usize| {
|
||||||
|
let validator = &state.validator_registry[index];
|
||||||
|
|
||||||
|
if validator.withdrawable_epoch != spec.far_future_epoch {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
current_epoch >= validator.exit_epoch + spec.min_validator_withdrawability_delay
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut eligable_indices: Vec<usize> = (0..state.validator_registry.len())
|
||||||
|
.filter(|i| eligible(*i))
|
||||||
|
.collect();
|
||||||
|
eligable_indices.sort_by_key(|i| state.validator_registry[*i].exit_epoch);
|
||||||
|
|
||||||
|
for (withdrawn_so_far, index) in eligable_indices.iter().enumerate() {
|
||||||
|
if withdrawn_so_far as u64 >= spec.max_exit_dequeues_per_epoch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prepare_validator_for_withdrawal(state, *index, spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initiate an exit for the validator of the given `index`.
|
||||||
|
///
|
||||||
|
/// Spec v0.5.0
|
||||||
|
fn prepare_validator_for_withdrawal(
|
||||||
|
state: &mut BeaconState,
|
||||||
|
validator_index: usize,
|
||||||
|
spec: &ChainSpec,
|
||||||
|
) {
|
||||||
|
state.validator_registry[validator_index].withdrawable_epoch =
|
||||||
|
state.current_epoch(spec) + spec.min_validator_withdrawability_delay;
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
use types::{BeaconStateError as Error, *};
|
||||||
|
|
||||||
|
/// Process slashings.
|
||||||
|
///
|
||||||
|
/// Note: Utilizes the cache and will fail if the appropriate cache is not initialized.
|
||||||
|
///
|
||||||
|
/// Spec v0.4.0
|
||||||
|
pub fn process_slashings(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), Error> {
|
||||||
|
let current_epoch = state.current_epoch(spec);
|
||||||
|
let active_validator_indices = state.get_active_validator_indices(current_epoch, spec)?;
|
||||||
|
let total_balance = state.get_total_balance(&active_validator_indices[..], spec)?;
|
||||||
|
|
||||||
|
for (index, validator) in state.validator_registry.iter().enumerate() {
|
||||||
|
if validator.slashed
|
||||||
|
&& (current_epoch
|
||||||
|
== validator.withdrawable_epoch - Epoch::from(spec.latest_slashed_exit_length / 2))
|
||||||
|
{
|
||||||
|
// TODO: check the following two lines are correct.
|
||||||
|
let total_at_start = state.get_slashed_balance(current_epoch + 1, spec)?;
|
||||||
|
let total_at_end = state.get_slashed_balance(current_epoch, spec)?;
|
||||||
|
|
||||||
|
let total_penalities = total_at_end.saturating_sub(total_at_start);
|
||||||
|
|
||||||
|
let effective_balance = state.get_effective_balance(index, spec)?;
|
||||||
|
let penalty = std::cmp::max(
|
||||||
|
effective_balance * std::cmp::min(total_penalities * 3, total_balance)
|
||||||
|
/ total_balance,
|
||||||
|
effective_balance / spec.min_penalty_quotient,
|
||||||
|
);
|
||||||
|
|
||||||
|
safe_sub_assign!(state.validator_balances[index], penalty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
use super::update_validator_registry::update_validator_registry;
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use types::*;
|
use types::*;
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ pub fn process_validator_registry(state: &mut BeaconState, spec: &ChainSpec) ->
|
|||||||
state.previous_shuffling_seed = state.current_shuffling_seed;
|
state.previous_shuffling_seed = state.current_shuffling_seed;
|
||||||
|
|
||||||
if should_update_validator_registry(state, spec)? {
|
if should_update_validator_registry(state, spec)? {
|
||||||
state.update_validator_registry(spec)?;
|
update_validator_registry(state, spec)?;
|
||||||
|
|
||||||
state.current_shuffling_epoch = next_epoch;
|
state.current_shuffling_epoch = next_epoch;
|
||||||
state.current_shuffling_start_shard = (state.current_shuffling_start_shard
|
state.current_shuffling_start_shard = (state.current_shuffling_start_shard
|
||||||
@ -37,9 +38,6 @@ pub fn process_validator_registry(state: &mut BeaconState, spec: &ChainSpec) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.process_slashings(spec)?;
|
|
||||||
state.process_exit_queue(spec);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
use crate::common::exit_validator;
|
||||||
|
use types::{BeaconStateError as Error, *};
|
||||||
|
|
||||||
|
/// Update validator registry, activating/exiting validators if possible.
|
||||||
|
///
|
||||||
|
/// Note: Utilizes the cache and will fail if the appropriate cache is not initialized.
|
||||||
|
///
|
||||||
|
/// Spec v0.4.0
|
||||||
|
pub fn update_validator_registry(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), Error> {
|
||||||
|
let current_epoch = state.current_epoch(spec);
|
||||||
|
let active_validator_indices = state.get_active_validator_indices(current_epoch, spec)?;
|
||||||
|
let total_balance = state.get_total_balance(&active_validator_indices[..], spec)?;
|
||||||
|
|
||||||
|
let max_balance_churn = std::cmp::max(
|
||||||
|
spec.max_deposit_amount,
|
||||||
|
total_balance / (2 * spec.max_balance_churn_quotient),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut balance_churn = 0;
|
||||||
|
for index in 0..state.validator_registry.len() {
|
||||||
|
let validator = &state.validator_registry[index];
|
||||||
|
|
||||||
|
if (validator.activation_epoch == spec.far_future_epoch)
|
||||||
|
& (state.validator_balances[index] == spec.max_deposit_amount)
|
||||||
|
{
|
||||||
|
balance_churn += state.get_effective_balance(index, spec)?;
|
||||||
|
if balance_churn > max_balance_churn {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
state.activate_validator(index, false, spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut balance_churn = 0;
|
||||||
|
for index in 0..state.validator_registry.len() {
|
||||||
|
let validator = &state.validator_registry[index];
|
||||||
|
|
||||||
|
if (validator.exit_epoch == spec.far_future_epoch) & (validator.initiated_exit) {
|
||||||
|
balance_churn += state.get_effective_balance(index, spec)?;
|
||||||
|
if balance_churn > max_balance_churn {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_validator(state, index, spec)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.validator_registry_update_epoch = current_epoch;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user