Merge branch 'master' into lighthouse-150
This commit is contained in:
commit
ee13c6ee40
@ -1,3 +1,4 @@
|
||||
use crate::cached_beacon_state::CachedBeaconState;
|
||||
use state_processing::validate_attestation_without_signature;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use types::{
|
||||
@ -76,12 +77,12 @@ impl AttestationAggregator {
|
||||
/// - The signature is verified against that of the validator at `validator_index`.
|
||||
pub fn process_free_attestation(
|
||||
&mut self,
|
||||
state: &BeaconState,
|
||||
cached_state: &CachedBeaconState,
|
||||
free_attestation: &FreeAttestation,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Outcome, BeaconStateError> {
|
||||
let (slot, shard, committee_index) = some_or_invalid!(
|
||||
state.attestation_slot_and_shard_for_validator(
|
||||
cached_state.attestation_slot_and_shard_for_validator(
|
||||
free_attestation.validator_index as usize,
|
||||
spec,
|
||||
)?,
|
||||
@ -104,7 +105,8 @@ impl AttestationAggregator {
|
||||
let signable_message = free_attestation.data.signable_message(PHASE_0_CUSTODY_BIT);
|
||||
|
||||
let validator_record = some_or_invalid!(
|
||||
state
|
||||
cached_state
|
||||
.state
|
||||
.validator_registry
|
||||
.get(free_attestation.validator_index as usize),
|
||||
Message::BadValidatorIndex
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::attestation_aggregator::{AttestationAggregator, Outcome as AggregationOutcome};
|
||||
use crate::cached_beacon_state::CachedBeaconState;
|
||||
use crate::checkpoint::CheckPoint;
|
||||
use db::{
|
||||
stores::{BeaconBlockStore, BeaconStateStore},
|
||||
@ -69,6 +70,7 @@ pub struct BeaconChain<T: ClientDB + Sized, U: SlotClock, F: ForkChoice> {
|
||||
canonical_head: RwLock<CheckPoint>,
|
||||
finalized_head: RwLock<CheckPoint>,
|
||||
pub state: RwLock<BeaconState>,
|
||||
pub cached_state: RwLock<CachedBeaconState>,
|
||||
pub spec: ChainSpec,
|
||||
pub fork_choice: RwLock<F>,
|
||||
}
|
||||
@ -107,6 +109,11 @@ where
|
||||
let block_root = genesis_block.canonical_root();
|
||||
block_store.put(&block_root, &ssz_encode(&genesis_block)[..])?;
|
||||
|
||||
let cached_state = RwLock::new(CachedBeaconState::from_beacon_state(
|
||||
genesis_state.clone(),
|
||||
spec.clone(),
|
||||
)?);
|
||||
|
||||
let finalized_head = RwLock::new(CheckPoint::new(
|
||||
genesis_block.clone(),
|
||||
block_root,
|
||||
@ -127,6 +134,7 @@ where
|
||||
slot_clock,
|
||||
attestation_aggregator,
|
||||
state: RwLock::new(genesis_state.clone()),
|
||||
cached_state,
|
||||
finalized_head,
|
||||
canonical_head,
|
||||
spec,
|
||||
@ -253,6 +261,7 @@ where
|
||||
/// Information is read from the present `beacon_state` shuffling, so only information from the
|
||||
/// present and prior epoch is available.
|
||||
pub fn block_proposer(&self, slot: Slot) -> Result<usize, BeaconStateError> {
|
||||
trace!("BeaconChain::block_proposer: slot: {}", slot);
|
||||
let index = self
|
||||
.state
|
||||
.read()
|
||||
@ -274,8 +283,12 @@ where
|
||||
&self,
|
||||
validator_index: usize,
|
||||
) -> Result<Option<(Slot, u64)>, BeaconStateError> {
|
||||
trace!(
|
||||
"BeaconChain::validator_attestion_slot_and_shard: validator_index: {}",
|
||||
validator_index
|
||||
);
|
||||
if let Some((slot, shard, _committee)) = self
|
||||
.state
|
||||
.cached_state
|
||||
.read()
|
||||
.attestation_slot_and_shard_for_validator(validator_index, &self.spec)?
|
||||
{
|
||||
@ -287,6 +300,7 @@ where
|
||||
|
||||
/// Produce an `AttestationData` that is valid for the present `slot` and given `shard`.
|
||||
pub fn produce_attestation_data(&self, shard: u64) -> Result<AttestationData, Error> {
|
||||
trace!("BeaconChain::produce_attestation_data: shard: {}", shard);
|
||||
let justified_epoch = self.justified_epoch();
|
||||
let justified_block_root = *self
|
||||
.state
|
||||
@ -332,9 +346,7 @@ where
|
||||
let aggregation_outcome = self
|
||||
.attestation_aggregator
|
||||
.write()
|
||||
.process_free_attestation(&self.state.read(), &free_attestation, &self.spec)?;
|
||||
// TODO: Check this comment
|
||||
//.map_err(|e| e.into())?;
|
||||
.process_free_attestation(&self.cached_state.read(), &free_attestation, &self.spec)?;
|
||||
|
||||
// return if the attestation is invalid
|
||||
if !aggregation_outcome.valid {
|
||||
@ -489,6 +501,9 @@ where
|
||||
);
|
||||
// Update the local state variable.
|
||||
*self.state.write() = state.clone();
|
||||
// Update the cached state variable.
|
||||
*self.cached_state.write() =
|
||||
CachedBeaconState::from_beacon_state(state.clone(), self.spec.clone())?;
|
||||
}
|
||||
|
||||
Ok(BlockProcessingOutcome::ValidBlock(ValidBlock::Processed))
|
||||
@ -537,9 +552,15 @@ where
|
||||
},
|
||||
};
|
||||
|
||||
state
|
||||
.per_block_processing_without_verifying_block_signature(&block, &self.spec)
|
||||
.ok()?;
|
||||
trace!("BeaconChain::produce_block: updating state for new block.",);
|
||||
|
||||
let result =
|
||||
state.per_block_processing_without_verifying_block_signature(&block, &self.spec);
|
||||
trace!(
|
||||
"BeaconNode::produce_block: state processing result: {:?}",
|
||||
result
|
||||
);
|
||||
result.ok()?;
|
||||
|
||||
let state_root = state.canonical_root();
|
||||
|
||||
|
150
beacon_node/beacon_chain/src/cached_beacon_state.rs
Normal file
150
beacon_node/beacon_chain/src/cached_beacon_state.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use log::{debug, trace};
|
||||
use std::collections::HashMap;
|
||||
use types::{beacon_state::BeaconStateError, BeaconState, ChainSpec, Epoch, Slot};
|
||||
|
||||
pub const CACHE_PREVIOUS: bool = false;
|
||||
pub const CACHE_CURRENT: bool = true;
|
||||
pub const CACHE_NEXT: bool = false;
|
||||
|
||||
pub type CrosslinkCommittees = Vec<(Vec<usize>, u64)>;
|
||||
pub type Shard = u64;
|
||||
pub type CommitteeIndex = u64;
|
||||
pub type AttestationDuty = (Slot, Shard, CommitteeIndex);
|
||||
pub type AttestationDutyMap = HashMap<u64, AttestationDuty>;
|
||||
|
||||
// TODO: CachedBeaconState is presently duplicating `BeaconState` and `ChainSpec`. This is a
|
||||
// massive memory waste, switch them to references.
|
||||
|
||||
pub struct CachedBeaconState {
|
||||
pub state: BeaconState,
|
||||
committees: Vec<Vec<CrosslinkCommittees>>,
|
||||
attestation_duties: Vec<AttestationDutyMap>,
|
||||
next_epoch: Epoch,
|
||||
current_epoch: Epoch,
|
||||
previous_epoch: Epoch,
|
||||
spec: ChainSpec,
|
||||
}
|
||||
|
||||
impl CachedBeaconState {
|
||||
pub fn from_beacon_state(
|
||||
state: BeaconState,
|
||||
spec: ChainSpec,
|
||||
) -> Result<Self, BeaconStateError> {
|
||||
let current_epoch = state.current_epoch(&spec);
|
||||
let previous_epoch = if current_epoch == spec.genesis_epoch {
|
||||
current_epoch
|
||||
} else {
|
||||
current_epoch.saturating_sub(1_u64)
|
||||
};
|
||||
let next_epoch = state.next_epoch(&spec);
|
||||
|
||||
let mut committees: Vec<Vec<CrosslinkCommittees>> = Vec::with_capacity(3);
|
||||
let mut attestation_duties: Vec<AttestationDutyMap> = Vec::with_capacity(3);
|
||||
|
||||
if CACHE_PREVIOUS {
|
||||
debug!("from_beacon_state: building previous epoch cache.");
|
||||
let cache = build_epoch_cache(&state, previous_epoch, &spec)?;
|
||||
committees.push(cache.committees);
|
||||
attestation_duties.push(cache.attestation_duty_map);
|
||||
} else {
|
||||
committees.push(vec![]);
|
||||
attestation_duties.push(HashMap::new());
|
||||
}
|
||||
if CACHE_CURRENT {
|
||||
debug!("from_beacon_state: building current epoch cache.");
|
||||
let cache = build_epoch_cache(&state, current_epoch, &spec)?;
|
||||
committees.push(cache.committees);
|
||||
attestation_duties.push(cache.attestation_duty_map);
|
||||
} else {
|
||||
committees.push(vec![]);
|
||||
attestation_duties.push(HashMap::new());
|
||||
}
|
||||
if CACHE_NEXT {
|
||||
debug!("from_beacon_state: building next epoch cache.");
|
||||
let cache = build_epoch_cache(&state, next_epoch, &spec)?;
|
||||
committees.push(cache.committees);
|
||||
attestation_duties.push(cache.attestation_duty_map);
|
||||
} else {
|
||||
committees.push(vec![]);
|
||||
attestation_duties.push(HashMap::new());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
committees,
|
||||
attestation_duties,
|
||||
next_epoch,
|
||||
current_epoch,
|
||||
previous_epoch,
|
||||
spec,
|
||||
})
|
||||
}
|
||||
|
||||
fn slot_to_cache_index(&self, slot: Slot) -> Option<usize> {
|
||||
trace!("slot_to_cache_index: cache lookup");
|
||||
match slot.epoch(self.spec.epoch_length) {
|
||||
epoch if (epoch == self.previous_epoch) & CACHE_PREVIOUS => Some(0),
|
||||
epoch if (epoch == self.current_epoch) & CACHE_CURRENT => Some(1),
|
||||
epoch if (epoch == self.next_epoch) & CACHE_NEXT => Some(2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `slot`, `shard` and `committee_index` for which a validator must produce an
|
||||
/// attestation.
|
||||
///
|
||||
/// Cached method.
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
pub fn attestation_slot_and_shard_for_validator(
|
||||
&self,
|
||||
validator_index: usize,
|
||||
_spec: &ChainSpec,
|
||||
) -> Result<Option<(Slot, u64, u64)>, BeaconStateError> {
|
||||
// Get the result for this epoch.
|
||||
let cache_index = self
|
||||
.slot_to_cache_index(self.state.slot)
|
||||
.expect("Current epoch should always have a cache index.");
|
||||
|
||||
let duties = self.attestation_duties[cache_index]
|
||||
.get(&(validator_index as u64))
|
||||
.and_then(|tuple| Some(*tuple));
|
||||
|
||||
Ok(duties)
|
||||
}
|
||||
}
|
||||
|
||||
struct EpochCacheResult {
|
||||
committees: Vec<CrosslinkCommittees>,
|
||||
attestation_duty_map: AttestationDutyMap,
|
||||
}
|
||||
|
||||
fn build_epoch_cache(
|
||||
state: &BeaconState,
|
||||
epoch: Epoch,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<EpochCacheResult, BeaconStateError> {
|
||||
let mut epoch_committees: Vec<CrosslinkCommittees> =
|
||||
Vec::with_capacity(spec.epoch_length as usize);
|
||||
let mut attestation_duty_map: AttestationDutyMap = HashMap::new();
|
||||
|
||||
for slot in epoch.slot_iter(spec.epoch_length) {
|
||||
let slot_committees = state.get_crosslink_committees_at_slot(slot, false, spec)?;
|
||||
|
||||
for (committee, shard) in slot_committees {
|
||||
for (committee_index, validator_index) in committee.iter().enumerate() {
|
||||
attestation_duty_map.insert(
|
||||
*validator_index as u64,
|
||||
(slot, shard, committee_index as u64),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
epoch_committees.push(state.get_crosslink_committees_at_slot(slot, false, spec)?)
|
||||
}
|
||||
|
||||
Ok(EpochCacheResult {
|
||||
committees: epoch_committees,
|
||||
attestation_duty_map,
|
||||
})
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
mod attestation_aggregator;
|
||||
mod beacon_chain;
|
||||
mod cached_beacon_state;
|
||||
mod checkpoint;
|
||||
|
||||
pub use self::beacon_chain::{BeaconChain, Error};
|
||||
|
@ -128,7 +128,18 @@ impl BeaconChainHarness {
|
||||
pub fn increment_beacon_chain_slot(&mut self) -> Slot {
|
||||
let slot = self.beacon_chain.present_slot() + 1;
|
||||
|
||||
debug!("Incrementing BeaconChain slot to {}.", slot);
|
||||
let nth_slot = slot
|
||||
- slot
|
||||
.epoch(self.spec.epoch_length)
|
||||
.start_slot(self.spec.epoch_length);
|
||||
let nth_epoch = slot.epoch(self.spec.epoch_length) - self.spec.genesis_epoch;
|
||||
debug!(
|
||||
"Advancing BeaconChain to slot {}, epoch {} (epoch height: {}, slot {} in epoch.).",
|
||||
slot,
|
||||
slot.epoch(self.spec.epoch_length),
|
||||
nth_epoch,
|
||||
nth_slot
|
||||
);
|
||||
|
||||
self.beacon_chain.slot_clock.set_slot(slot.as_u64());
|
||||
self.beacon_chain.advance_state(slot).unwrap();
|
||||
@ -209,6 +220,7 @@ impl BeaconChainHarness {
|
||||
self.increment_beacon_chain_slot();
|
||||
|
||||
// Produce a new block.
|
||||
debug!("Producing block...");
|
||||
let block = self.produce_block();
|
||||
debug!("Submitting block for processing...");
|
||||
self.beacon_chain.process_block(block).unwrap();
|
||||
|
@ -1,19 +1,14 @@
|
||||
use env_logger::{Builder, Env};
|
||||
use log::debug;
|
||||
use test_harness::BeaconChainHarness;
|
||||
use types::{ChainSpec, Slot};
|
||||
use types::ChainSpec;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn it_can_build_on_genesis_block() {
|
||||
let mut spec = ChainSpec::foundation();
|
||||
spec.genesis_slot = Slot::new(spec.epoch_length * 8);
|
||||
Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
/*
|
||||
spec.shard_count = spec.shard_count / 8;
|
||||
spec.target_committee_size = spec.target_committee_size / 8;
|
||||
*/
|
||||
let validator_count = 1000;
|
||||
let spec = ChainSpec::few_validators();
|
||||
let validator_count = 8;
|
||||
|
||||
let mut harness = BeaconChainHarness::new(spec, validator_count as usize);
|
||||
|
||||
@ -23,21 +18,22 @@ fn it_can_build_on_genesis_block() {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn it_can_produce_past_first_epoch_boundary() {
|
||||
Builder::from_env(Env::default().default_filter_or("debug")).init();
|
||||
Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
let validator_count = 100;
|
||||
let spec = ChainSpec::few_validators();
|
||||
let validator_count = 8;
|
||||
|
||||
debug!("Starting harness build...");
|
||||
|
||||
let mut harness = BeaconChainHarness::new(ChainSpec::foundation(), validator_count);
|
||||
let mut harness = BeaconChainHarness::new(spec, validator_count);
|
||||
|
||||
debug!("Harness built, tests starting..");
|
||||
|
||||
let blocks = harness.spec.epoch_length * 3 + 1;
|
||||
let blocks = harness.spec.epoch_length * 2 + 1;
|
||||
|
||||
for i in 0..blocks {
|
||||
harness.advance_chain_with_block();
|
||||
debug!("Produced block {}/{}.", i, blocks);
|
||||
debug!("Produced block {}/{}.", i + 1, blocks);
|
||||
}
|
||||
let dump = harness.chain_dump().expect("Chain dump failed.");
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::SlotProcessingError;
|
||||
use hashing::hash;
|
||||
use int_to_bytes::int_to_bytes32;
|
||||
use log::debug;
|
||||
use log::{debug, trace};
|
||||
use ssz::{ssz_encode, TreeHash};
|
||||
use types::{
|
||||
beacon_state::{AttestationParticipantsError, BeaconStateError},
|
||||
@ -219,6 +219,8 @@ fn per_block_processing_signature_optional(
|
||||
Error::MaxAttestationsExceeded
|
||||
);
|
||||
|
||||
debug!("Verifying {} attestations.", block.body.attestations.len());
|
||||
|
||||
for attestation in &block.body.attestations {
|
||||
validate_attestation(&state, attestation, spec)?;
|
||||
|
||||
@ -231,11 +233,6 @@ fn per_block_processing_signature_optional(
|
||||
state.latest_attestations.push(pending_attestation);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"{} attestations verified & processed.",
|
||||
block.body.attestations.len()
|
||||
);
|
||||
|
||||
/*
|
||||
* Deposits
|
||||
*/
|
||||
@ -312,6 +309,10 @@ fn validate_attestation_signature_optional(
|
||||
spec: &ChainSpec,
|
||||
verify_signature: bool,
|
||||
) -> Result<(), AttestationValidationError> {
|
||||
trace!(
|
||||
"validate_attestation_signature_optional: attestation epoch: {}",
|
||||
attestation.data.slot.epoch(spec.epoch_length)
|
||||
);
|
||||
ensure!(
|
||||
attestation.data.slot + spec.min_attestation_inclusion_delay <= state.slot,
|
||||
AttestationValidationError::IncludedTooEarly
|
||||
|
@ -144,8 +144,10 @@ impl EpochProcessable for BeaconState {
|
||||
|
||||
let previous_epoch_attester_indices =
|
||||
self.get_attestation_participants_union(&previous_epoch_attestations[..], spec)?;
|
||||
let previous_total_balance =
|
||||
self.get_total_balance(&previous_epoch_attester_indices[..], spec);
|
||||
let previous_total_balance = self.get_total_balance(
|
||||
&get_active_validator_indices(&self.validator_registry, previous_epoch),
|
||||
spec,
|
||||
);
|
||||
|
||||
/*
|
||||
* Validators targetting the previous justified slot
|
||||
@ -315,6 +317,11 @@ impl EpochProcessable for BeaconState {
|
||||
|
||||
// for slot in self.slot.saturating_sub(2 * spec.epoch_length)..self.slot {
|
||||
for slot in self.previous_epoch(spec).slot_iter(spec.epoch_length) {
|
||||
trace!(
|
||||
"Finding winning root for slot: {} (epoch: {})",
|
||||
slot,
|
||||
slot.epoch(spec.epoch_length)
|
||||
);
|
||||
let crosslink_committees_at_slot =
|
||||
self.get_crosslink_committees_at_slot(slot, false, spec)?;
|
||||
|
||||
@ -352,7 +359,8 @@ impl EpochProcessable for BeaconState {
|
||||
/*
|
||||
* Rewards and Penalities
|
||||
*/
|
||||
let base_reward_quotient = previous_total_balance.integer_sqrt();
|
||||
let base_reward_quotient =
|
||||
previous_total_balance.integer_sqrt() / spec.base_reward_quotient;
|
||||
if base_reward_quotient == 0 {
|
||||
return Err(Error::BaseRewardQuotientIsZero);
|
||||
}
|
||||
@ -539,6 +547,12 @@ impl EpochProcessable for BeaconState {
|
||||
*/
|
||||
self.previous_calculation_epoch = self.current_calculation_epoch;
|
||||
self.previous_epoch_start_shard = self.current_epoch_start_shard;
|
||||
|
||||
debug!(
|
||||
"setting previous_epoch_seed to : {}",
|
||||
self.current_epoch_seed
|
||||
);
|
||||
|
||||
self.previous_epoch_seed = self.current_epoch_seed;
|
||||
|
||||
let should_update_validator_registy = if self.finalized_epoch
|
||||
@ -553,6 +567,7 @@ impl EpochProcessable for BeaconState {
|
||||
};
|
||||
|
||||
if should_update_validator_registy {
|
||||
trace!("updating validator registry.");
|
||||
self.update_validator_registry(spec);
|
||||
|
||||
self.current_calculation_epoch = next_epoch;
|
||||
@ -561,6 +576,7 @@ impl EpochProcessable for BeaconState {
|
||||
% spec.shard_count;
|
||||
self.current_epoch_seed = self.generate_seed(self.current_calculation_epoch, spec)?
|
||||
} else {
|
||||
trace!("not updating validator registry.");
|
||||
let epochs_since_last_registry_update =
|
||||
current_epoch - self.validator_registry_update_epoch;
|
||||
if (epochs_since_last_registry_update > 1)
|
||||
|
@ -18,7 +18,7 @@ serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
slog = "^2.2.3"
|
||||
ssz = { path = "../utils/ssz" }
|
||||
fisher_yates_shuffle = { path = "../utils/fisher_yates_shuffle" }
|
||||
swap_or_not_shuffle = { path = "../utils/swap_or_not_shuffle" }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.6.0"
|
||||
|
@ -5,15 +5,19 @@ use crate::{
|
||||
PendingAttestation, PublicKey, Signature, Slot, Validator,
|
||||
};
|
||||
use bls::verify_proof_of_possession;
|
||||
use fisher_yates_shuffle::shuffle;
|
||||
use honey_badger_split::SplitExt;
|
||||
use log::trace;
|
||||
use rand::RngCore;
|
||||
use serde_derive::Serialize;
|
||||
use ssz::{hash, Decodable, DecodeError, Encodable, SszStream, TreeHash};
|
||||
use swap_or_not_shuffle::get_permutated_index;
|
||||
|
||||
mod tests;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum BeaconStateError {
|
||||
EpochOutOfBounds,
|
||||
UnableToShuffle,
|
||||
InsufficientRandaoMixes,
|
||||
InsufficientValidators,
|
||||
InsufficientBlockRoots,
|
||||
@ -201,7 +205,12 @@ impl BeaconState {
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
pub fn previous_epoch(&self, spec: &ChainSpec) -> Epoch {
|
||||
self.current_epoch(spec).saturating_sub(1_u64)
|
||||
let current_epoch = self.current_epoch(&spec);
|
||||
if current_epoch == spec.genesis_epoch {
|
||||
current_epoch
|
||||
} else {
|
||||
current_epoch - 1
|
||||
}
|
||||
}
|
||||
|
||||
/// The epoch following `self.current_epoch()`.
|
||||
@ -249,23 +258,50 @@ impl BeaconState {
|
||||
/// committee is itself a list of validator indices.
|
||||
///
|
||||
/// Spec v0.1
|
||||
pub fn get_shuffling(&self, seed: Hash256, epoch: Epoch, spec: &ChainSpec) -> Vec<Vec<usize>> {
|
||||
pub fn get_shuffling(
|
||||
&self,
|
||||
seed: Hash256,
|
||||
epoch: Epoch,
|
||||
spec: &ChainSpec,
|
||||
) -> Option<Vec<Vec<usize>>> {
|
||||
let active_validator_indices =
|
||||
get_active_validator_indices(&self.validator_registry, epoch);
|
||||
|
||||
if active_validator_indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
trace!(
|
||||
"get_shuffling: active_validator_indices.len() == {}",
|
||||
active_validator_indices.len()
|
||||
);
|
||||
|
||||
let committees_per_epoch =
|
||||
self.get_epoch_committee_count(active_validator_indices.len(), spec);
|
||||
|
||||
// TODO: check that Hash256::from(u64) matches 'int_to_bytes32'.
|
||||
let seed = seed ^ Hash256::from(epoch.as_u64());
|
||||
// TODO: fix `expect` assert.
|
||||
let shuffled_active_validator_indices =
|
||||
shuffle(&seed, active_validator_indices).expect("Max validator count exceed!");
|
||||
trace!(
|
||||
"get_shuffling: active_validator_indices.len() == {}, committees_per_epoch: {}",
|
||||
active_validator_indices.len(),
|
||||
committees_per_epoch
|
||||
);
|
||||
|
||||
shuffled_active_validator_indices
|
||||
.honey_badger_split(committees_per_epoch as usize)
|
||||
.map(|slice: &[usize]| slice.to_vec())
|
||||
.collect()
|
||||
let mut shuffled_active_validator_indices = vec![0; active_validator_indices.len()];
|
||||
for &i in &active_validator_indices {
|
||||
let shuffled_i = get_permutated_index(
|
||||
i,
|
||||
active_validator_indices.len(),
|
||||
&seed[..],
|
||||
spec.shuffle_round_count,
|
||||
)?;
|
||||
shuffled_active_validator_indices[i] = active_validator_indices[shuffled_i]
|
||||
}
|
||||
|
||||
Some(
|
||||
shuffled_active_validator_indices
|
||||
.honey_badger_split(committees_per_epoch as usize)
|
||||
.map(|slice: &[usize]| slice.to_vec())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the number of committees in the previous epoch.
|
||||
@ -303,9 +339,17 @@ impl BeaconState {
|
||||
+ 1;
|
||||
let latest_index_root = current_epoch + spec.entry_exit_delay;
|
||||
|
||||
trace!(
|
||||
"get_active_index_root: epoch: {}, earliest: {}, latest: {}",
|
||||
epoch,
|
||||
earliest_index_root,
|
||||
latest_index_root
|
||||
);
|
||||
|
||||
if (epoch >= earliest_index_root) & (epoch <= latest_index_root) {
|
||||
Some(self.latest_index_roots[epoch.as_usize() % spec.latest_index_roots_length])
|
||||
} else {
|
||||
trace!("get_active_index_root: epoch out of range.");
|
||||
None
|
||||
}
|
||||
}
|
||||
@ -350,29 +394,28 @@ impl BeaconState {
|
||||
) -> Result<Vec<(Vec<usize>, u64)>, BeaconStateError> {
|
||||
let epoch = slot.epoch(spec.epoch_length);
|
||||
let current_epoch = self.current_epoch(spec);
|
||||
let previous_epoch = if current_epoch == spec.genesis_epoch {
|
||||
current_epoch
|
||||
} else {
|
||||
current_epoch.saturating_sub(1_u64)
|
||||
};
|
||||
let previous_epoch = self.previous_epoch(spec);
|
||||
let next_epoch = self.next_epoch(spec);
|
||||
|
||||
let (committees_per_epoch, seed, shuffling_epoch, shuffling_start_shard) =
|
||||
if epoch == previous_epoch {
|
||||
(
|
||||
self.get_previous_epoch_committee_count(spec),
|
||||
self.previous_epoch_seed,
|
||||
self.previous_calculation_epoch,
|
||||
self.previous_epoch_start_shard,
|
||||
)
|
||||
} else if epoch == current_epoch {
|
||||
if epoch == current_epoch {
|
||||
trace!("get_crosslink_committees_at_slot: current_epoch");
|
||||
(
|
||||
self.get_current_epoch_committee_count(spec),
|
||||
self.current_epoch_seed,
|
||||
self.current_calculation_epoch,
|
||||
self.current_epoch_start_shard,
|
||||
)
|
||||
} else if epoch == previous_epoch {
|
||||
trace!("get_crosslink_committees_at_slot: previous_epoch");
|
||||
(
|
||||
self.get_previous_epoch_committee_count(spec),
|
||||
self.previous_epoch_seed,
|
||||
self.previous_calculation_epoch,
|
||||
self.previous_epoch_start_shard,
|
||||
)
|
||||
} else if epoch == next_epoch {
|
||||
trace!("get_crosslink_committees_at_slot: next_epoch");
|
||||
let current_committees_per_epoch = self.get_current_epoch_committee_count(spec);
|
||||
let epochs_since_last_registry_update =
|
||||
current_epoch - self.validator_registry_update_epoch;
|
||||
@ -401,12 +444,21 @@ impl BeaconState {
|
||||
return Err(BeaconStateError::EpochOutOfBounds);
|
||||
};
|
||||
|
||||
let shuffling = self.get_shuffling(seed, shuffling_epoch, spec);
|
||||
let shuffling = self
|
||||
.get_shuffling(seed, shuffling_epoch, spec)
|
||||
.ok_or_else(|| BeaconStateError::UnableToShuffle)?;
|
||||
let offset = slot.as_u64() % spec.epoch_length;
|
||||
let committees_per_slot = committees_per_epoch / spec.epoch_length;
|
||||
let slot_start_shard =
|
||||
(shuffling_start_shard + committees_per_slot * offset) % spec.shard_count;
|
||||
|
||||
trace!(
|
||||
"get_crosslink_committees_at_slot: committees_per_slot: {}, slot_start_shard: {}, seed: {}",
|
||||
committees_per_slot,
|
||||
slot_start_shard,
|
||||
seed
|
||||
);
|
||||
|
||||
let mut crosslinks_at_slot = vec![];
|
||||
for i in 0..committees_per_slot {
|
||||
let tuple = (
|
||||
@ -458,6 +510,11 @@ impl BeaconState {
|
||||
spec: &ChainSpec,
|
||||
) -> Result<usize, BeaconStateError> {
|
||||
let committees = self.get_crosslink_committees_at_slot(slot, false, spec)?;
|
||||
trace!(
|
||||
"get_beacon_proposer_index: slot: {}, committees_count: {}",
|
||||
slot,
|
||||
committees.len()
|
||||
);
|
||||
committees
|
||||
.first()
|
||||
.ok_or(BeaconStateError::InsufficientValidators)
|
||||
@ -1064,33 +1121,3 @@ impl<T: RngCore> TestRandom<T> for BeaconState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
|
||||
use ssz::ssz_encode;
|
||||
|
||||
#[test]
|
||||
pub fn test_ssz_round_trip() {
|
||||
let mut rng = XorShiftRng::from_seed([42; 16]);
|
||||
let original = BeaconState::random_for_test(&mut rng);
|
||||
|
||||
let bytes = ssz_encode(&original);
|
||||
let (decoded, _) = <_>::ssz_decode(&bytes, 0).unwrap();
|
||||
|
||||
assert_eq!(original, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_tree_root() {
|
||||
let mut rng = XorShiftRng::from_seed([42; 16]);
|
||||
let original = BeaconState::random_for_test(&mut rng);
|
||||
|
||||
let result = original.hash_tree_root();
|
||||
|
||||
assert_eq!(result.len(), 32);
|
||||
// TODO: Add further tests
|
||||
// https://github.com/sigp/lighthouse/issues/170
|
||||
}
|
||||
}
|
||||
|
97
eth2/types/src/beacon_state/tests.rs
Normal file
97
eth2/types/src/beacon_state/tests.rs
Normal file
@ -0,0 +1,97 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
|
||||
use crate::{
|
||||
beacon_state::BeaconStateError, BeaconState, ChainSpec, Deposit, DepositData, DepositInput,
|
||||
Eth1Data, Hash256, Keypair,
|
||||
};
|
||||
use bls::create_proof_of_possession;
|
||||
use ssz::ssz_encode;
|
||||
|
||||
struct BeaconStateTestBuilder {
|
||||
pub genesis_time: u64,
|
||||
pub initial_validator_deposits: Vec<Deposit>,
|
||||
pub latest_eth1_data: Eth1Data,
|
||||
pub spec: ChainSpec,
|
||||
pub keypairs: Vec<Keypair>,
|
||||
}
|
||||
|
||||
impl BeaconStateTestBuilder {
|
||||
pub fn with_random_validators(validator_count: usize) -> Self {
|
||||
let genesis_time = 10_000_000;
|
||||
let keypairs: Vec<Keypair> = (0..validator_count)
|
||||
.collect::<Vec<usize>>()
|
||||
.iter()
|
||||
.map(|_| Keypair::random())
|
||||
.collect();
|
||||
let initial_validator_deposits = keypairs
|
||||
.iter()
|
||||
.map(|keypair| Deposit {
|
||||
branch: vec![], // branch verification is not specified.
|
||||
index: 0, // index verification is not specified.
|
||||
deposit_data: DepositData {
|
||||
amount: 32_000_000_000, // 32 ETH (in Gwei)
|
||||
timestamp: genesis_time - 1,
|
||||
deposit_input: DepositInput {
|
||||
pubkey: keypair.pk.clone(),
|
||||
withdrawal_credentials: Hash256::zero(), // Withdrawal not possible.
|
||||
proof_of_possession: create_proof_of_possession(&keypair),
|
||||
},
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
let latest_eth1_data = Eth1Data {
|
||||
deposit_root: Hash256::zero(),
|
||||
block_hash: Hash256::zero(),
|
||||
};
|
||||
let spec = ChainSpec::foundation();
|
||||
|
||||
Self {
|
||||
genesis_time,
|
||||
initial_validator_deposits,
|
||||
latest_eth1_data,
|
||||
spec,
|
||||
keypairs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Result<BeaconState, BeaconStateError> {
|
||||
BeaconState::genesis(
|
||||
self.genesis_time,
|
||||
self.initial_validator_deposits.clone(),
|
||||
self.latest_eth1_data.clone(),
|
||||
&self.spec,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_produce_genesis_block() {
|
||||
let builder = BeaconStateTestBuilder::with_random_validators(2);
|
||||
|
||||
builder.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_ssz_round_trip() {
|
||||
let mut rng = XorShiftRng::from_seed([42; 16]);
|
||||
let original = BeaconState::random_for_test(&mut rng);
|
||||
|
||||
let bytes = ssz_encode(&original);
|
||||
let (decoded, _) = <_>::ssz_decode(&bytes, 0).unwrap();
|
||||
|
||||
assert_eq!(original, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_tree_root() {
|
||||
let mut rng = XorShiftRng::from_seed([42; 16]);
|
||||
let original = BeaconState::random_for_test(&mut rng);
|
||||
|
||||
let result = original.hash_tree_root();
|
||||
|
||||
assert_eq!(result.len(), 32);
|
||||
// TODO: Add further tests
|
||||
// https://github.com/sigp/lighthouse/issues/170
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
beacon_state::BeaconStateError, BeaconState, ChainSpec, Deposit, DepositData, DepositInput,
|
||||
Eth1Data, Hash256, Keypair,
|
||||
};
|
||||
use bls::create_proof_of_possession;
|
||||
|
||||
struct BeaconStateTestBuilder {
|
||||
pub genesis_time: u64,
|
||||
pub initial_validator_deposits: Vec<Deposit>,
|
||||
pub latest_eth1_data: Eth1Data,
|
||||
pub spec: ChainSpec,
|
||||
pub keypairs: Vec<Keypair>,
|
||||
}
|
||||
|
||||
impl BeaconStateTestBuilder {
|
||||
pub fn with_random_validators(validator_count: usize) -> Self {
|
||||
let genesis_time = 10_000_000;
|
||||
let keypairs: Vec<Keypair> = (0..validator_count)
|
||||
.collect::<Vec<usize>>()
|
||||
.iter()
|
||||
.map(|_| Keypair::random())
|
||||
.collect();
|
||||
let initial_validator_deposits = keypairs
|
||||
.iter()
|
||||
.map(|keypair| Deposit {
|
||||
branch: vec![], // branch verification is not specified.
|
||||
index: 0, // index verification is not specified.
|
||||
deposit_data: DepositData {
|
||||
amount: 32_000_000_000, // 32 ETH (in Gwei)
|
||||
timestamp: genesis_time - 1,
|
||||
deposit_input: DepositInput {
|
||||
pubkey: keypair.pk.clone(),
|
||||
withdrawal_credentials: Hash256::zero(), // Withdrawal not possible.
|
||||
proof_of_possession: create_proof_of_possession(&keypair),
|
||||
},
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
let latest_eth1_data = Eth1Data {
|
||||
deposit_root: Hash256::zero(),
|
||||
block_hash: Hash256::zero(),
|
||||
};
|
||||
let spec = ChainSpec::foundation();
|
||||
|
||||
Self {
|
||||
genesis_time,
|
||||
initial_validator_deposits,
|
||||
latest_eth1_data,
|
||||
spec,
|
||||
keypairs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Result<BeaconState, BeaconStateError> {
|
||||
BeaconState::genesis(
|
||||
self.genesis_time,
|
||||
self.initial_validator_deposits.clone(),
|
||||
self.latest_eth1_data.clone(),
|
||||
&self.spec,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_produce_genesis_block() {
|
||||
let builder = BeaconStateTestBuilder::with_random_validators(2);
|
||||
|
||||
builder.build().unwrap();
|
||||
}
|
||||
}
|
@ -1,7 +1,96 @@
|
||||
use crate::{Address, ChainSpec, Epoch, Hash256, Signature, Slot};
|
||||
use crate::{Address, Epoch, Hash256, Slot};
|
||||
use bls::Signature;
|
||||
|
||||
const GWEI: u64 = 1_000_000_000;
|
||||
|
||||
/// Holds all the "constants" for a BeaconChain.
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct ChainSpec {
|
||||
/*
|
||||
* Misc
|
||||
*/
|
||||
pub shard_count: u64,
|
||||
pub target_committee_size: u64,
|
||||
pub max_balance_churn_quotient: u64,
|
||||
pub beacon_chain_shard_number: u64,
|
||||
pub max_indices_per_slashable_vote: u64,
|
||||
pub max_withdrawals_per_epoch: u64,
|
||||
pub shuffle_round_count: u8,
|
||||
|
||||
/*
|
||||
* Deposit contract
|
||||
*/
|
||||
pub deposit_contract_address: Address,
|
||||
pub deposit_contract_tree_depth: u64,
|
||||
|
||||
/*
|
||||
* Gwei values
|
||||
*/
|
||||
pub min_deposit_amount: u64,
|
||||
pub max_deposit_amount: u64,
|
||||
pub fork_choice_balance_increment: u64,
|
||||
pub ejection_balance: u64,
|
||||
|
||||
/*
|
||||
* Initial Values
|
||||
*/
|
||||
pub genesis_fork_version: u64,
|
||||
pub genesis_slot: Slot,
|
||||
pub genesis_epoch: Epoch,
|
||||
pub genesis_start_shard: u64,
|
||||
pub far_future_epoch: Epoch,
|
||||
pub zero_hash: Hash256,
|
||||
pub empty_signature: Signature,
|
||||
pub bls_withdrawal_prefix_byte: u8,
|
||||
|
||||
/*
|
||||
* Time parameters
|
||||
*/
|
||||
pub slot_duration: u64,
|
||||
pub min_attestation_inclusion_delay: u64,
|
||||
pub epoch_length: u64,
|
||||
pub seed_lookahead: Epoch,
|
||||
pub entry_exit_delay: u64,
|
||||
pub eth1_data_voting_period: u64,
|
||||
pub min_validator_withdrawal_epochs: Epoch,
|
||||
|
||||
/*
|
||||
* State list lengths
|
||||
*/
|
||||
pub latest_block_roots_length: usize,
|
||||
pub latest_randao_mixes_length: usize,
|
||||
pub latest_index_roots_length: usize,
|
||||
pub latest_penalized_exit_length: usize,
|
||||
|
||||
/*
|
||||
* Reward and penalty quotients
|
||||
*/
|
||||
pub base_reward_quotient: u64,
|
||||
pub whistleblower_reward_quotient: u64,
|
||||
pub includer_reward_quotient: u64,
|
||||
pub inactivity_penalty_quotient: u64,
|
||||
|
||||
/*
|
||||
* Max operations per block
|
||||
*/
|
||||
pub max_proposer_slashings: u64,
|
||||
pub max_attester_slashings: u64,
|
||||
pub max_attestations: u64,
|
||||
pub max_deposits: u64,
|
||||
pub max_exits: u64,
|
||||
|
||||
/*
|
||||
* Signature domains
|
||||
*/
|
||||
pub domain_deposit: u64,
|
||||
pub domain_attestation: u64,
|
||||
pub domain_proposal: u64,
|
||||
pub domain_exit: u64,
|
||||
pub domain_randao: u64,
|
||||
}
|
||||
|
||||
impl ChainSpec {
|
||||
/// Returns a `ChainSpec` compatible with the specification from Ethereum Foundation.
|
||||
///
|
||||
@ -100,6 +189,26 @@ impl ChainSpec {
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainSpec {
|
||||
/// Returns a `ChainSpec` compatible with the specification suitable for 8 validators.
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
pub fn few_validators() -> Self {
|
||||
let genesis_slot = Slot::new(2_u64.pow(19));
|
||||
let epoch_length = 8;
|
||||
let genesis_epoch = genesis_slot.epoch(epoch_length);
|
||||
|
||||
Self {
|
||||
shard_count: 1,
|
||||
target_committee_size: 1,
|
||||
genesis_slot,
|
||||
genesis_epoch,
|
||||
epoch_length,
|
||||
..ChainSpec::foundation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
@ -7,8 +7,8 @@ pub mod attester_slashing;
|
||||
pub mod beacon_block;
|
||||
pub mod beacon_block_body;
|
||||
pub mod beacon_state;
|
||||
pub mod beacon_state_tests;
|
||||
pub mod casper_slashing;
|
||||
pub mod chain_spec;
|
||||
pub mod crosslink;
|
||||
pub mod deposit;
|
||||
pub mod deposit_data;
|
||||
@ -29,7 +29,6 @@ pub mod slashable_vote_data;
|
||||
pub mod slot_epoch_macros;
|
||||
pub mod slot_epoch;
|
||||
pub mod slot_height;
|
||||
pub mod spec;
|
||||
pub mod validator;
|
||||
pub mod validator_registry;
|
||||
pub mod validator_registry_delta_block;
|
||||
@ -45,6 +44,7 @@ pub use crate::beacon_block::BeaconBlock;
|
||||
pub use crate::beacon_block_body::BeaconBlockBody;
|
||||
pub use crate::beacon_state::BeaconState;
|
||||
pub use crate::casper_slashing::CasperSlashing;
|
||||
pub use crate::chain_spec::ChainSpec;
|
||||
pub use crate::crosslink::Crosslink;
|
||||
pub use crate::deposit::Deposit;
|
||||
pub use crate::deposit_data::DepositData;
|
||||
@ -61,7 +61,6 @@ pub use crate::slashable_attestation::SlashableAttestation;
|
||||
pub use crate::slashable_vote_data::SlashableVoteData;
|
||||
pub use crate::slot_epoch::{Epoch, Slot};
|
||||
pub use crate::slot_height::SlotHeight;
|
||||
pub use crate::spec::ChainSpec;
|
||||
pub use crate::validator::{StatusFlags as ValidatorStatusFlags, Validator};
|
||||
pub use crate::validator_registry_delta_block::ValidatorRegistryDeltaBlock;
|
||||
|
||||
|
@ -1,92 +0,0 @@
|
||||
mod foundation;
|
||||
|
||||
use crate::{Address, Epoch, Hash256, Slot};
|
||||
use bls::Signature;
|
||||
|
||||
/// Holds all the "constants" for a BeaconChain.
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct ChainSpec {
|
||||
/*
|
||||
* Misc
|
||||
*/
|
||||
pub shard_count: u64,
|
||||
pub target_committee_size: u64,
|
||||
pub max_balance_churn_quotient: u64,
|
||||
pub beacon_chain_shard_number: u64,
|
||||
pub max_indices_per_slashable_vote: u64,
|
||||
pub max_withdrawals_per_epoch: u64,
|
||||
pub shuffle_round_count: u64,
|
||||
|
||||
/*
|
||||
* Deposit contract
|
||||
*/
|
||||
pub deposit_contract_address: Address,
|
||||
pub deposit_contract_tree_depth: u64,
|
||||
|
||||
/*
|
||||
* Gwei values
|
||||
*/
|
||||
pub min_deposit_amount: u64,
|
||||
pub max_deposit_amount: u64,
|
||||
pub fork_choice_balance_increment: u64,
|
||||
pub ejection_balance: u64,
|
||||
|
||||
/*
|
||||
* Initial Values
|
||||
*/
|
||||
pub genesis_fork_version: u64,
|
||||
pub genesis_slot: Slot,
|
||||
pub genesis_epoch: Epoch,
|
||||
pub genesis_start_shard: u64,
|
||||
pub far_future_epoch: Epoch,
|
||||
pub zero_hash: Hash256,
|
||||
pub empty_signature: Signature,
|
||||
pub bls_withdrawal_prefix_byte: u8,
|
||||
|
||||
/*
|
||||
* Time parameters
|
||||
*/
|
||||
pub slot_duration: u64,
|
||||
pub min_attestation_inclusion_delay: u64,
|
||||
pub epoch_length: u64,
|
||||
pub seed_lookahead: Epoch,
|
||||
pub entry_exit_delay: u64,
|
||||
pub eth1_data_voting_period: u64,
|
||||
pub min_validator_withdrawal_epochs: Epoch,
|
||||
|
||||
/*
|
||||
* State list lengths
|
||||
*/
|
||||
pub latest_block_roots_length: usize,
|
||||
pub latest_randao_mixes_length: usize,
|
||||
pub latest_index_roots_length: usize,
|
||||
pub latest_penalized_exit_length: usize,
|
||||
|
||||
/*
|
||||
* Reward and penalty quotients
|
||||
*/
|
||||
pub base_reward_quotient: u64,
|
||||
pub whistleblower_reward_quotient: u64,
|
||||
pub includer_reward_quotient: u64,
|
||||
pub inactivity_penalty_quotient: u64,
|
||||
|
||||
/*
|
||||
* Max operations per block
|
||||
*/
|
||||
pub max_proposer_slashings: u64,
|
||||
pub max_attester_slashings: u64,
|
||||
pub max_attestations: u64,
|
||||
pub max_deposits: u64,
|
||||
pub max_exits: u64,
|
||||
|
||||
/*
|
||||
* Signature domains
|
||||
*/
|
||||
pub domain_deposit: u64,
|
||||
pub domain_attestation: u64,
|
||||
pub domain_proposal: u64,
|
||||
pub domain_exit: u64,
|
||||
pub domain_randao: u64,
|
||||
}
|
Loading…
Reference in New Issue
Block a user