e8604757a2
## Summary The deposit cache now has the ability to finalize deposits. This will cause it to drop unneeded deposit logs and hashes in the deposit Merkle tree that are no longer required to construct deposit proofs. The cache is finalized whenever the latest finalized checkpoint has a new `Eth1Data` with all deposits imported. This has three benefits: 1. Improves the speed of constructing Merkle proofs for deposits as we can just replay deposits since the last finalized checkpoint instead of all historical deposits when re-constructing the Merkle tree. 2. Significantly faster weak subjectivity sync as the deposit cache can be transferred to the newly syncing node in compressed form. The Merkle tree that stores `N` finalized deposits requires a maximum of `log2(N)` hashes. The newly syncing node then only needs to download deposits since the last finalized checkpoint to have a full tree. 3. Future proofing in preparation for [EIP-4444](https://eips.ethereum.org/EIPS/eip-4444) as execution nodes will no longer be required to store logs permanently so we won't always have all historical logs available to us. ## More Details Image to illustrate how the deposit contract merkle tree evolves and finalizes along with the resulting `DepositTreeSnapshot` ![image](https://user-images.githubusercontent.com/37123614/151465302-5fc56284-8a69-4998-b20e-45db3934ac70.png) ## Other Considerations I've changed the structure of the `SszDepositCache` so once you load & save your database from this version of lighthouse, you will no longer be able to load it from older versions. Co-authored-by: ethDreamer <37123614+ethDreamer@users.noreply.github.com>
136 lines
4.5 KiB
Rust
136 lines
4.5 KiB
Rust
use crate::service::endpoint_from_config;
|
|
use crate::Config;
|
|
use crate::{
|
|
block_cache::{BlockCache, Eth1Block},
|
|
deposit_cache::{DepositCache, SszDepositCache, SszDepositCacheV1, SszDepositCacheV13},
|
|
};
|
|
use execution_layer::HttpJsonRpc;
|
|
use parking_lot::RwLock;
|
|
use ssz::four_byte_option_impl;
|
|
use ssz::{Decode, Encode};
|
|
use ssz_derive::{Decode, Encode};
|
|
use superstruct::superstruct;
|
|
use types::{ChainSpec, DepositTreeSnapshot, Eth1Data};
|
|
|
|
// Define "legacy" implementations of `Option<u64>` which use four bytes for encoding the union
|
|
// selector.
|
|
four_byte_option_impl!(four_byte_option_u64, u64);
|
|
|
|
#[derive(Default)]
|
|
pub struct DepositUpdater {
|
|
pub cache: DepositCache,
|
|
pub last_processed_block: Option<u64>,
|
|
}
|
|
|
|
impl DepositUpdater {
|
|
pub fn new(deposit_contract_deploy_block: u64) -> Self {
|
|
let cache = DepositCache::new(deposit_contract_deploy_block);
|
|
DepositUpdater {
|
|
cache,
|
|
last_processed_block: None,
|
|
}
|
|
}
|
|
|
|
pub fn from_snapshot(
|
|
deposit_contract_deploy_block: u64,
|
|
snapshot: &DepositTreeSnapshot,
|
|
) -> Result<Self, String> {
|
|
let last_processed_block = Some(snapshot.execution_block_height);
|
|
Ok(Self {
|
|
cache: DepositCache::from_deposit_snapshot(deposit_contract_deploy_block, snapshot)?,
|
|
last_processed_block,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct Inner {
|
|
pub block_cache: RwLock<BlockCache>,
|
|
pub deposit_cache: RwLock<DepositUpdater>,
|
|
pub endpoint: HttpJsonRpc,
|
|
// this gets set to Some(Eth1Data) when the deposit finalization conditions are met
|
|
pub to_finalize: RwLock<Option<Eth1Data>>,
|
|
pub config: RwLock<Config>,
|
|
pub remote_head_block: RwLock<Option<Eth1Block>>,
|
|
pub spec: ChainSpec,
|
|
}
|
|
|
|
impl Inner {
|
|
/// Prunes the block cache to `self.target_block_cache_len`.
|
|
///
|
|
/// Is a no-op if `self.target_block_cache_len` is `None`.
|
|
pub fn prune_blocks(&self) {
|
|
if let Some(block_cache_truncation) = self.config.read().block_cache_truncation {
|
|
self.block_cache.write().truncate(block_cache_truncation);
|
|
}
|
|
}
|
|
|
|
/// Encode the eth1 block and deposit cache as bytes.
|
|
pub fn as_bytes(&self) -> Vec<u8> {
|
|
let ssz_eth1_cache = SszEth1Cache::from_inner(self);
|
|
ssz_eth1_cache.as_ssz_bytes()
|
|
}
|
|
|
|
/// Recover `Inner` given byte representation of eth1 deposit and block caches.
|
|
pub fn from_bytes(bytes: &[u8], config: Config, spec: ChainSpec) -> Result<Self, String> {
|
|
SszEth1Cache::from_ssz_bytes(bytes)
|
|
.map_err(|e| format!("Ssz decoding error: {:?}", e))?
|
|
.to_inner(config, spec)
|
|
.map(|inner| {
|
|
inner.block_cache.write().rebuild_by_hash_map();
|
|
inner
|
|
})
|
|
}
|
|
|
|
/// Returns a reference to the specification.
|
|
pub fn spec(&self) -> &ChainSpec {
|
|
&self.spec
|
|
}
|
|
}
|
|
|
|
pub type SszEth1Cache = SszEth1CacheV13;
|
|
|
|
#[superstruct(
|
|
variants(V1, V13),
|
|
variant_attributes(derive(Encode, Decode, Clone)),
|
|
no_enum
|
|
)]
|
|
pub struct SszEth1Cache {
|
|
pub block_cache: BlockCache,
|
|
#[superstruct(only(V1))]
|
|
pub deposit_cache: SszDepositCacheV1,
|
|
#[superstruct(only(V13))]
|
|
pub deposit_cache: SszDepositCacheV13,
|
|
#[ssz(with = "four_byte_option_u64")]
|
|
pub last_processed_block: Option<u64>,
|
|
}
|
|
|
|
impl SszEth1Cache {
|
|
pub fn from_inner(inner: &Inner) -> Self {
|
|
let deposit_updater = inner.deposit_cache.read();
|
|
let block_cache = inner.block_cache.read();
|
|
Self {
|
|
block_cache: (*block_cache).clone(),
|
|
deposit_cache: SszDepositCache::from_deposit_cache(&deposit_updater.cache),
|
|
last_processed_block: deposit_updater.last_processed_block,
|
|
}
|
|
}
|
|
|
|
pub fn to_inner(&self, config: Config, spec: ChainSpec) -> Result<Inner, String> {
|
|
Ok(Inner {
|
|
block_cache: RwLock::new(self.block_cache.clone()),
|
|
deposit_cache: RwLock::new(DepositUpdater {
|
|
cache: self.deposit_cache.to_deposit_cache()?,
|
|
last_processed_block: self.last_processed_block,
|
|
}),
|
|
endpoint: endpoint_from_config(&config)
|
|
.map_err(|e| format!("Failed to create endpoint: {:?}", e))?,
|
|
to_finalize: RwLock::new(None),
|
|
// Set the remote head_block zero when creating a new instance. We only care about
|
|
// present and future eth1 nodes.
|
|
remote_head_block: RwLock::new(None),
|
|
config: RwLock::new(config),
|
|
spec,
|
|
})
|
|
}
|
|
}
|