Single-pass epoch processing (#4483)

This commit is contained in:
Michael Sproul
2023-07-18 16:59:55 +10:00
committed by GitHub
parent 079cd67df2
commit 5d2063d262
42 changed files with 1558 additions and 730 deletions

View File

@@ -1,5 +1,4 @@
use self::committee_cache::get_active_validator_indices;
use self::exit_cache::ExitCache;
use crate::test_utils::TestRandom;
use crate::validator::ValidatorTrait;
use crate::*;
@@ -29,7 +28,9 @@ pub use self::committee_cache::{
CommitteeCache,
};
pub use crate::beacon_state::balance::Balance;
pub use crate::beacon_state::exit_cache::ExitCache;
pub use crate::beacon_state::progressive_balances_cache::*;
pub use crate::beacon_state::slashings_cache::SlashingsCache;
use crate::epoch_cache::EpochCache;
use crate::historical_summary::HistoricalSummary;
pub use eth_spec::*;
@@ -44,6 +45,7 @@ mod exit_cache;
mod iter;
mod progressive_balances_cache;
mod pubkey_cache;
mod slashings_cache;
mod tests;
pub const CACHED_EPOCHS: usize = 3;
@@ -103,6 +105,10 @@ pub enum Error {
},
RelativeEpochError(RelativeEpochError),
ExitCacheUninitialized,
SlashingsCacheUninitialized {
initialized_slot: Option<Slot>,
latest_block_slot: Slot,
},
CommitteeCacheUninitialized(Option<RelativeEpoch>),
SyncCommitteeCacheUninitialized,
BlsError(bls::Error),
@@ -153,6 +159,7 @@ pub enum Error {
TotalActiveBalanceDiffUninitialized,
MissingImmutableValidator(usize),
IndexNotSupported(usize),
InvalidFlagIndex(usize),
MerkleTreeError(merkle_proof::MerkleTreeError),
}
@@ -449,6 +456,12 @@ where
#[test_random(default)]
#[metastruct(exclude)]
pub exit_cache: ExitCache,
#[serde(skip_serializing, skip_deserializing)]
#[ssz(skip_serializing, skip_deserializing)]
#[tree_hash(skip_hashing)]
#[test_random(default)]
#[metastruct(exclude)]
pub slashings_cache: SlashingsCache,
/// Epoch cache of values that are useful for block processing that are static over an epoch.
#[serde(skip_serializing, skip_deserializing)]
#[ssz(skip_serializing, skip_deserializing)]
@@ -516,6 +529,7 @@ impl<T: EthSpec> BeaconState<T> {
],
pubkey_cache: PubkeyCache::default(),
exit_cache: ExitCache::default(),
slashings_cache: SlashingsCache::default(),
epoch_cache: EpochCache::default(),
})
}
@@ -1301,6 +1315,57 @@ impl<T: EthSpec> BeaconState<T> {
}
}
#[allow(clippy::type_complexity)]
pub fn mutable_validator_fields(
&mut self,
) -> Result<
(
&mut Validators<T>,
&mut Balances<T>,
&VList<ParticipationFlags, T::ValidatorRegistryLimit>,
&VList<ParticipationFlags, T::ValidatorRegistryLimit>,
&mut VList<u64, T::ValidatorRegistryLimit>,
&mut ProgressiveBalancesCache,
&mut ExitCache,
&mut EpochCache,
),
Error,
> {
match self {
BeaconState::Base(_) => Err(Error::IncorrectStateVariant),
BeaconState::Altair(state) => Ok((
&mut state.validators,
&mut state.balances,
&state.previous_epoch_participation,
&state.current_epoch_participation,
&mut state.inactivity_scores,
&mut state.progressive_balances_cache,
&mut state.exit_cache,
&mut state.epoch_cache,
)),
BeaconState::Merge(state) => Ok((
&mut state.validators,
&mut state.balances,
&state.previous_epoch_participation,
&state.current_epoch_participation,
&mut state.inactivity_scores,
&mut state.progressive_balances_cache,
&mut state.exit_cache,
&mut state.epoch_cache,
)),
BeaconState::Capella(state) => Ok((
&mut state.validators,
&mut state.balances,
&state.previous_epoch_participation,
&state.current_epoch_participation,
&mut state.inactivity_scores,
&mut state.progressive_balances_cache,
&mut state.exit_cache,
&mut state.epoch_cache,
)),
}
}
/// Get a mutable reference to the balance of a single validator.
pub fn get_balance_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> {
self.balances_mut()
@@ -1400,7 +1465,7 @@ impl<T: EthSpec> BeaconState<T> {
epoch: Epoch,
spec: &ChainSpec,
) -> Result<Epoch, Error> {
Ok(epoch.safe_add(1)?.safe_add(spec.max_seed_lookahead)?)
Ok(spec.compute_activation_exit_epoch(epoch)?)
}
/// Return the churn limit for the current epoch (number of validators who can leave per epoch).
@@ -1574,6 +1639,7 @@ impl<T: EthSpec> BeaconState<T> {
self.build_all_committee_caches(spec)?;
self.update_pubkey_cache()?;
self.build_exit_cache(spec)?;
self.build_slashings_cache()?;
Ok(())
}
@@ -1594,6 +1660,20 @@ impl<T: EthSpec> BeaconState<T> {
Ok(())
}
/// Build the slashings cache if it needs to be built.
pub fn build_slashings_cache(&mut self) -> Result<(), Error> {
let latest_block_slot = self.latest_block_header().slot;
if !self.slashings_cache().is_initialized(latest_block_slot) {
*self.slashings_cache_mut() = SlashingsCache::new(latest_block_slot, self.validators());
}
Ok(())
}
pub fn slashings_cache_is_initialized(&self) -> bool {
let latest_block_slot = self.latest_block_header().slot;
self.slashings_cache().is_initialized(latest_block_slot)
}
/// Drop all caches on the state.
pub fn drop_all_caches(&mut self) -> Result<(), Error> {
self.drop_total_active_balance_cache();
@@ -1603,6 +1683,7 @@ impl<T: EthSpec> BeaconState<T> {
self.drop_pubkey_cache();
self.drop_progressive_balances_cache();
*self.exit_cache_mut() = ExitCache::default();
*self.slashings_cache_mut() = SlashingsCache::default();
*self.epoch_cache_mut() = EpochCache::default();
Ok(())
}

View File

@@ -52,6 +52,7 @@ macro_rules! full_to_compact {
progressive_balances_cache: $s.progressive_balances_cache.clone(),
pubkey_cache: $s.pubkey_cache.clone(),
exit_cache: $s.exit_cache.clone(),
slashings_cache: $s.slashings_cache.clone(),
epoch_cache: $s.epoch_cache.clone(),
// Variant-specific fields
@@ -114,6 +115,7 @@ macro_rules! compact_to_full {
progressive_balances_cache: $inner.progressive_balances_cache,
pubkey_cache: $inner.pubkey_cache,
exit_cache: $inner.exit_cache,
slashings_cache: $inner.slashings_cache,
epoch_cache: $inner.epoch_cache,
// Variant-specific fields

View File

@@ -1,5 +1,11 @@
use crate::beacon_state::balance::Balance;
use crate::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec};
use crate::{
consts::altair::{
NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX,
TIMELY_TARGET_FLAG_INDEX,
},
BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags,
};
use arbitrary::Arbitrary;
use safe_arith::SafeArith;
use serde_derive::{Deserialize, Serialize};
@@ -17,21 +23,110 @@ pub struct ProgressiveBalancesCache {
#[derive(Debug, PartialEq, Arbitrary, Clone)]
struct Inner {
pub current_epoch: Epoch,
pub previous_epoch_target_attesting_balance: Balance,
pub current_epoch_target_attesting_balance: Balance,
pub previous_epoch_cache: EpochTotalBalances,
pub current_epoch_cache: EpochTotalBalances,
}
/// Caches the participation values for one epoch (either the previous or current).
#[derive(PartialEq, Debug, Clone, Arbitrary)]
pub struct EpochTotalBalances {
/// Stores the sum of the balances for all validators in `self.unslashed_participating_indices`
/// for all flags in `NUM_FLAG_INDICES`.
///
/// A flag balance is only incremented if a validator is in that flag set.
pub total_flag_balances: [Balance; NUM_FLAG_INDICES],
}
impl EpochTotalBalances {
pub fn new(spec: &ChainSpec) -> Self {
let zero_balance = Balance::zero(spec.effective_balance_increment);
Self {
total_flag_balances: [zero_balance; NUM_FLAG_INDICES],
}
}
/// Returns the total balance of attesters who have `flag_index` set.
pub fn total_flag_balance(&self, flag_index: usize) -> Result<u64, BeaconStateError> {
self.total_flag_balances
.get(flag_index)
.map(Balance::get)
.ok_or(BeaconStateError::InvalidFlagIndex(flag_index))
}
/// Returns the raw total balance of attesters who have `flag_index` set.
pub fn total_flag_balance_raw(&self, flag_index: usize) -> Result<Balance, BeaconStateError> {
self.total_flag_balances
.get(flag_index)
.copied()
.ok_or(BeaconStateError::InvalidFlagIndex(flag_index))
}
pub fn on_new_attestation(
&mut self,
flag_index: usize,
validator_effective_balance: u64,
) -> Result<(), BeaconStateError> {
let balance = self
.total_flag_balances
.get_mut(flag_index)
.ok_or(BeaconStateError::InvalidFlagIndex(flag_index))?;
balance.safe_add_assign(validator_effective_balance)?;
Ok(())
}
pub fn on_slashing(
&mut self,
participation_flags: ParticipationFlags,
validator_effective_balance: u64,
) -> Result<(), BeaconStateError> {
for flag_index in 0..NUM_FLAG_INDICES {
if participation_flags.has_flag(flag_index)? {
self.total_flag_balances
.get_mut(flag_index)
.ok_or(BeaconStateError::InvalidFlagIndex(flag_index))?
.safe_sub_assign(validator_effective_balance)?;
}
}
Ok(())
}
pub fn on_effective_balance_change(
&mut self,
current_epoch_participation_flags: ParticipationFlags,
old_effective_balance: u64,
new_effective_balance: u64,
) -> Result<(), BeaconStateError> {
for flag_index in 0..NUM_FLAG_INDICES {
if current_epoch_participation_flags.has_flag(flag_index)? {
let total = self
.total_flag_balances
.get_mut(flag_index)
.ok_or(BeaconStateError::InvalidFlagIndex(flag_index))?;
if new_effective_balance > old_effective_balance {
total
.safe_add_assign(new_effective_balance.safe_sub(old_effective_balance)?)?;
} else {
total
.safe_sub_assign(old_effective_balance.safe_sub(new_effective_balance)?)?;
}
}
}
Ok(())
}
}
impl ProgressiveBalancesCache {
pub fn initialize(
&mut self,
current_epoch: Epoch,
previous_epoch_target_attesting_balance: Balance,
current_epoch_target_attesting_balance: Balance,
previous_epoch_cache: EpochTotalBalances,
current_epoch_cache: EpochTotalBalances,
) {
self.inner = Some(Inner {
current_epoch,
previous_epoch_target_attesting_balance,
current_epoch_target_attesting_balance,
previous_epoch_cache,
current_epoch_cache,
});
}
@@ -39,24 +134,31 @@ impl ProgressiveBalancesCache {
self.inner.is_some()
}
pub fn is_initialized_at(&self, epoch: Epoch) -> bool {
self.inner
.as_ref()
.map_or(false, |inner| inner.current_epoch == epoch)
}
/// When a new target attestation has been processed, we update the cached
/// `current_epoch_target_attesting_balance` to include the validator effective balance.
/// If the epoch is neither the current epoch nor the previous epoch, an error is returned.
pub fn on_new_target_attestation(
pub fn on_new_attestation(
&mut self,
epoch: Epoch,
flag_index: usize,
validator_effective_balance: u64,
) -> Result<(), BeaconStateError> {
let cache = self.get_inner_mut()?;
if epoch == cache.current_epoch {
cache
.current_epoch_target_attesting_balance
.safe_add_assign(validator_effective_balance)?;
.current_epoch_cache
.on_new_attestation(flag_index, validator_effective_balance)?;
} else if epoch.safe_add(1)? == cache.current_epoch {
cache
.previous_epoch_target_attesting_balance
.safe_add_assign(validator_effective_balance)?;
.previous_epoch_cache
.on_new_attestation(flag_index, validator_effective_balance)?;
} else {
return Err(BeaconStateError::ProgressiveBalancesCacheInconsistent);
}
@@ -68,21 +170,17 @@ impl ProgressiveBalancesCache {
/// validator's effective balance to exclude the validator weight.
pub fn on_slashing(
&mut self,
is_previous_epoch_target_attester: bool,
is_current_epoch_target_attester: bool,
previous_epoch_participation: ParticipationFlags,
current_epoch_participation: ParticipationFlags,
effective_balance: u64,
) -> Result<(), BeaconStateError> {
let cache = self.get_inner_mut()?;
if is_previous_epoch_target_attester {
cache
.previous_epoch_target_attesting_balance
.safe_sub_assign(effective_balance)?;
}
if is_current_epoch_target_attester {
cache
.current_epoch_target_attesting_balance
.safe_sub_assign(effective_balance)?;
}
cache
.previous_epoch_cache
.on_slashing(previous_epoch_participation, effective_balance)?;
cache
.current_epoch_cache
.on_slashing(current_epoch_participation, effective_balance)?;
Ok(())
}
@@ -90,22 +188,16 @@ impl ProgressiveBalancesCache {
/// its share of the target attesting balance in the cache.
pub fn on_effective_balance_change(
&mut self,
is_current_epoch_target_attester: bool,
current_epoch_participation: ParticipationFlags,
old_effective_balance: u64,
new_effective_balance: u64,
) -> Result<(), BeaconStateError> {
let cache = self.get_inner_mut()?;
if is_current_epoch_target_attester {
if new_effective_balance > old_effective_balance {
cache
.current_epoch_target_attesting_balance
.safe_add_assign(new_effective_balance.safe_sub(old_effective_balance)?)?;
} else {
cache
.current_epoch_target_attesting_balance
.safe_sub_assign(old_effective_balance.safe_sub(new_effective_balance)?)?;
}
}
cache.current_epoch_cache.on_effective_balance_change(
current_epoch_participation,
old_effective_balance,
new_effective_balance,
)?;
Ok(())
}
@@ -114,25 +206,53 @@ impl ProgressiveBalancesCache {
pub fn on_epoch_transition(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> {
let cache = self.get_inner_mut()?;
cache.current_epoch.safe_add_assign(1)?;
cache.previous_epoch_target_attesting_balance =
cache.current_epoch_target_attesting_balance;
cache.current_epoch_target_attesting_balance =
Balance::zero(spec.effective_balance_increment);
cache.previous_epoch_cache = std::mem::replace(
&mut cache.current_epoch_cache,
EpochTotalBalances::new(spec),
);
Ok(())
}
pub fn previous_epoch_flag_attesting_balance(
&self,
flag_index: usize,
) -> Result<u64, BeaconStateError> {
self.get_inner()?
.previous_epoch_cache
.total_flag_balance(flag_index)
}
pub fn current_epoch_flag_attesting_balance(
&self,
flag_index: usize,
) -> Result<u64, BeaconStateError> {
self.get_inner()?
.current_epoch_cache
.total_flag_balance(flag_index)
}
pub fn previous_epoch_source_attesting_balance(&self) -> Result<u64, BeaconStateError> {
self.previous_epoch_flag_attesting_balance(TIMELY_SOURCE_FLAG_INDEX)
}
pub fn previous_epoch_target_attesting_balance(&self) -> Result<u64, BeaconStateError> {
Ok(self
.get_inner()?
.previous_epoch_target_attesting_balance
.get())
self.previous_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX)
}
pub fn previous_epoch_head_attesting_balance(&self) -> Result<u64, BeaconStateError> {
self.previous_epoch_flag_attesting_balance(TIMELY_HEAD_FLAG_INDEX)
}
pub fn current_epoch_source_attesting_balance(&self) -> Result<u64, BeaconStateError> {
self.current_epoch_flag_attesting_balance(TIMELY_SOURCE_FLAG_INDEX)
}
pub fn current_epoch_target_attesting_balance(&self) -> Result<u64, BeaconStateError> {
Ok(self
.get_inner()?
.current_epoch_target_attesting_balance
.get())
self.current_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX)
}
pub fn current_epoch_head_attesting_balance(&self) -> Result<u64, BeaconStateError> {
self.current_epoch_flag_attesting_balance(TIMELY_HEAD_FLAG_INDEX)
}
fn get_inner_mut(&mut self) -> Result<&mut Inner, BeaconStateError> {
@@ -158,7 +278,7 @@ pub enum ProgressiveBalancesMode {
/// Enable the usage of progressive cache, with checks against the `ParticipationCache` and falls
/// back to the existing calculation if there is a balance mismatch.
Checked,
/// Enable the usage of progressive cache, with checks against the `ParticipationCache`. Errors
/// Enable the usage of progressive cache, with checks against the `ParticipationCache`. BeaconStateErrors
/// if there is a balance mismatch. Used in testing only.
Strict,
/// Enable the usage of progressive cache, with no comparative checks against the

View File

@@ -0,0 +1,63 @@
use crate::{BeaconStateError, Slot, Validator};
use arbitrary::Arbitrary;
use rpds::HashTrieSetSync as HashTrieSet;
/// Persistent (cheap to clone) cache of all slashed validator indices.
#[derive(Debug, Default, Clone, PartialEq, Arbitrary)]
pub struct SlashingsCache {
latest_block_slot: Option<Slot>,
#[arbitrary(default)]
slashed_validators: HashTrieSet<usize>,
}
impl SlashingsCache {
/// Initialize a new cache for the given list of validators.
pub fn new<'a, V, I>(latest_block_slot: Slot, validators: V) -> Self
where
V: IntoIterator<Item = &'a Validator, IntoIter = I>,
I: ExactSizeIterator + Iterator<Item = &'a Validator>,
{
let slashed_validators = validators
.into_iter()
.enumerate()
.filter_map(|(i, validator)| validator.slashed().then_some(i))
.collect();
Self {
latest_block_slot: Some(latest_block_slot),
slashed_validators,
}
}
pub fn is_initialized(&self, slot: Slot) -> bool {
self.latest_block_slot == Some(slot)
}
pub fn check_initialized(&self, latest_block_slot: Slot) -> Result<(), BeaconStateError> {
if self.is_initialized(latest_block_slot) {
Ok(())
} else {
Err(BeaconStateError::SlashingsCacheUninitialized {
initialized_slot: self.latest_block_slot,
latest_block_slot,
})
}
}
pub fn record_validator_slashing(
&mut self,
block_slot: Slot,
validator_index: usize,
) -> Result<(), BeaconStateError> {
self.check_initialized(block_slot)?;
self.slashed_validators.insert_mut(validator_index);
Ok(())
}
pub fn is_slashed(&self, validator_index: usize) -> bool {
self.slashed_validators.contains(&validator_index)
}
pub fn update_latest_block_slot(&mut self, latest_block_slot: Slot) {
self.latest_block_slot = Some(latest_block_slot);
}
}

View File

@@ -1,6 +1,7 @@
use crate::application_domain::{ApplicationDomain, APPLICATION_DOMAIN_BUILDER};
use crate::*;
use int_to_bytes::int_to_bytes4;
use safe_arith::{ArithError, SafeArith};
use serde::{Deserializer, Serialize, Serializer};
use serde_derive::Deserialize;
use serde_utils::quoted_u64::MaybeQuoted;
@@ -285,12 +286,17 @@ impl ChainSpec {
}
/// For a given `BeaconState`, return the inactivity penalty quotient associated with its variant.
// FIXME(sproul): delete once unused
pub fn inactivity_penalty_quotient_for_state<T: EthSpec>(&self, state: &BeaconState<T>) -> u64 {
match state {
BeaconState::Base(_) => self.inactivity_penalty_quotient,
BeaconState::Altair(_) => self.inactivity_penalty_quotient_altair,
BeaconState::Merge(_) => self.inactivity_penalty_quotient_bellatrix,
BeaconState::Capella(_) => self.inactivity_penalty_quotient_bellatrix,
self.inactivity_penalty_quotient_for_fork(state.fork_name_unchecked())
}
pub fn inactivity_penalty_quotient_for_fork(&self, fork_name: ForkName) -> u64 {
match fork_name {
ForkName::Base => self.inactivity_penalty_quotient,
ForkName::Altair => self.inactivity_penalty_quotient_altair,
ForkName::Merge => self.inactivity_penalty_quotient_bellatrix,
ForkName::Capella => self.inactivity_penalty_quotient_bellatrix,
}
}
@@ -458,6 +464,10 @@ impl ChainSpec {
Hash256::from(domain)
}
pub fn compute_activation_exit_epoch(&self, epoch: Epoch) -> Result<Epoch, ArithError> {
epoch.safe_add(1)?.safe_add(self.max_seed_lookahead)
}
#[allow(clippy::integer_arithmetic)]
pub const fn attestation_subnet_prefix_bits(&self) -> u32 {
let attestation_subnet_count_bits = self.attestation_subnet_count.ilog2();

View File

@@ -1,5 +1,5 @@
use crate::{ActivationQueue, BeaconStateError, Epoch, EthSpec, Hash256, Slot};
use safe_arith::ArithError;
use crate::{ActivationQueue, BeaconStateError, ChainSpec, Epoch, EthSpec, Hash256, Slot};
use safe_arith::{ArithError, SafeArith};
use std::sync::Arc;
/// Cache of values which are uniquely determined at the start of an epoch.
@@ -18,10 +18,16 @@ struct Inner {
/// Unique identifier for this cache, which can be used to check its validity before use
/// with any `BeaconState`.
key: EpochCacheKey,
/// Base reward for every validator in this epoch.
/// Effective balance for every validator in this epoch.
effective_balances: Vec<u64>,
/// Base rewards for every effective balance increment (currently 0..32 ETH).
///
/// Keyed by `effective_balance / effective_balance_increment`.
base_rewards: Vec<u64>,
/// Validator activation queue.
activation_queue: ActivationQueue,
/// Effective balance increment.
effective_balance_increment: u64,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, arbitrary::Arbitrary)]
@@ -35,6 +41,7 @@ pub enum EpochCacheError {
IncorrectEpoch { cache: Epoch, state: Epoch },
IncorrectDecisionBlock { cache: Hash256, state: Hash256 },
ValidatorIndexOutOfBounds { validator_index: usize },
EffectiveBalanceOutOfBounds { effective_balance_eth: usize },
InvalidSlot { slot: Slot },
Arith(ArithError),
BeaconState(BeaconStateError),
@@ -56,14 +63,18 @@ impl From<ArithError> for EpochCacheError {
impl EpochCache {
pub fn new(
key: EpochCacheKey,
effective_balances: Vec<u64>,
base_rewards: Vec<u64>,
activation_queue: ActivationQueue,
spec: &ChainSpec,
) -> EpochCache {
Self {
inner: Some(Arc::new(Inner {
key,
effective_balances,
base_rewards,
activation_queue,
effective_balance_increment: spec.effective_balance_increment,
})),
}
}
@@ -93,16 +104,34 @@ impl EpochCache {
}
#[inline]
pub fn get_base_reward(&self, validator_index: usize) -> Result<u64, EpochCacheError> {
pub fn get_effective_balance(&self, validator_index: usize) -> Result<u64, EpochCacheError> {
self.inner
.as_ref()
.ok_or(EpochCacheError::CacheNotInitialized)?
.base_rewards
.effective_balances
.get(validator_index)
.copied()
.ok_or(EpochCacheError::ValidatorIndexOutOfBounds { validator_index })
}
#[inline]
pub fn get_base_reward(&self, validator_index: usize) -> Result<u64, EpochCacheError> {
let inner = self
.inner
.as_ref()
.ok_or(EpochCacheError::CacheNotInitialized)?;
let effective_balance = self.get_effective_balance(validator_index)?;
let effective_balance_eth =
effective_balance.safe_div(inner.effective_balance_increment)? as usize;
inner
.base_rewards
.get(effective_balance_eth)
.copied()
.ok_or(EpochCacheError::EffectiveBalanceOutOfBounds {
effective_balance_eth,
})
}
pub fn activation_queue(&self) -> Result<&ActivationQueue, EpochCacheError> {
let inner = self
.inner