diff --git a/parachain/Cargo.lock b/parachain/Cargo.lock index e9777ce158..a908e722ec 100644 --- a/parachain/Cargo.lock +++ b/parachain/Cargo.lock @@ -3561,6 +3561,7 @@ dependencies = [ "milagro_bls", "pallet-timestamp", "parity-scale-codec", + "rand 0.8.5", "rlp", "scale-info", "serde", diff --git a/parachain/pallets/ethereum-beacon-client/Cargo.toml b/parachain/pallets/ethereum-beacon-client/Cargo.toml index 1f232a5d71..8c5a22052c 100644 --- a/parachain/pallets/ethereum-beacon-client/Cargo.toml +++ b/parachain/pallets/ethereum-beacon-client/Cargo.toml @@ -34,6 +34,7 @@ snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = f snowbridge-beacon-primitives = { path = "../../primitives/beacon", default-features = false } [dev-dependencies] +rand = "0.8.5" sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "master" } snowbridge-testutils = { path = "../../primitives/testutils" } serde_json = "1.0.96" diff --git a/parachain/pallets/ethereum-beacon-client/src/lib.rs b/parachain/pallets/ethereum-beacon-client/src/lib.rs index f868d4c05a..768c9775cc 100644 --- a/parachain/pallets/ethereum-beacon-client/src/lib.rs +++ b/parachain/pallets/ethereum-beacon-client/src/lib.rs @@ -97,8 +97,14 @@ pub mod pallet { type MaxSlotsPerHistoricalRoot: Get; #[pallet::constant] type MaxFinalizedHeaderSlotArray: Get; + /// Maximum execution headers to be stored + #[pallet::constant] + type ExecutionHeadersPruneThreshold: Get; #[pallet::constant] type ForkVersions: Get; + /// Maximum sync committees to be stored + #[pallet::constant] + type SyncCommitteePruneThreshold: Get; type WeightInfo: WeightInfo; type WeakSubjectivityPeriodSeconds: Get; } @@ -165,21 +171,31 @@ pub mod pallet { #[pallet::storage] pub(super) type FinalizedBeaconHeaderSlots = - StorageValue<_, BoundedVec, ValueQuery>; + StorageValue<_, BoundedVec<(u64, H256), T::MaxFinalizedHeaderSlotArray>, ValueQuery>; #[pallet::storage] pub(super) type FinalizedBeaconHeadersBlockRoot = StorageMap<_, Identity, H256, H256, ValueQuery>; + /// Header mapping bounds. Used by pruning algorithm to prune oldest value and add + /// latest value. + #[pallet::storage] + pub(super) type ExecutionHeadersOldestMapping = + StorageValue<_, (u64, u64), OptionQuery>; + + /// Mapping of count -> Execution header hash. Used to prune older headers + #[pallet::storage] + pub(super) type ExecutionHeadersMapping = + StorageMap<_, Identity, u64, H256, ValueQuery>; + #[pallet::storage] pub(super) type ExecutionHeaders = - StorageMap<_, Identity, H256, ExecutionHeader, OptionQuery>; + CountedStorageMap<_, Identity, H256, ExecutionHeader, OptionQuery>; /// Current sync committee corresponding to the active header. - /// TODO prune older sync committees than xxx #[pallet::storage] pub(super) type SyncCommittees = - StorageMap<_, Identity, u64, SyncCommitteeOf, ValueQuery>; + CountedStorageMap<_, Identity, u64, SyncCommitteeOf, ValueQuery>; #[pallet::storage] pub(super) type ValidatorsRoot = StorageValue<_, H256, ValueQuery>; @@ -384,7 +400,7 @@ pub mod pallet { }; >::insert(block_root, initial_sync.header); - Self::add_finalized_header_slot(slot)?; + Self::add_finalized_header_slot(slot, block_root)?; >::set(last_finalized_header); Ok(()) @@ -864,7 +880,7 @@ pub mod pallet { Ok(()) } - fn store_sync_committee(period: u64, sync_committee: SyncCommitteeOf) { + pub(crate) fn store_sync_committee(period: u64, sync_committee: SyncCommitteeOf) { >::insert(period, sync_committee); log::trace!( @@ -874,15 +890,39 @@ pub mod pallet { ); >::set(period); + Self::prune_older_sync_committees(); Self::deposit_event(Event::SyncCommitteeUpdated { period }); } + // Contract: It is assumed that the light client do not skip sync committee. + fn prune_older_sync_committees() { + let threshold = T::SyncCommitteePruneThreshold::get(); + let stored_sync_committees = >::count(); + + if stored_sync_committees as u64 > threshold { + let latest_sync_committee_period = >::get(); + let highest_period_to_remove = latest_sync_committee_period - threshold; + + let mut current_sync_committee_to_remove = highest_period_to_remove; + let mut number_of_sync_committees_to_remove = + stored_sync_committees as u64 - threshold; + + while number_of_sync_committees_to_remove > 0 { + >::remove(current_sync_committee_to_remove); + number_of_sync_committees_to_remove = + number_of_sync_committees_to_remove.saturating_sub(1); + current_sync_committee_to_remove = + current_sync_committee_to_remove.saturating_sub(1); + } + } + } + fn store_finalized_header(block_root: Root, header: BeaconHeader) -> DispatchResult { let slot = header.slot; >::insert(block_root, header); - Self::add_finalized_header_slot(slot)?; + Self::add_finalized_header_slot(slot, block_root)?; log::info!( target: "ethereum-beacon-client", @@ -902,19 +942,27 @@ pub mod pallet { Ok(()) } - fn add_finalized_header_slot(slot: u64) -> DispatchResult { + pub(super) fn add_finalized_header_slot( + slot: u64, + finalized_header_hash: H256, + ) -> DispatchResult { >::try_mutate(|b_vec| { if b_vec.len() as u32 == T::MaxFinalizedHeaderSlotArray::get() { - b_vec.remove(0); + let (_slot, finalized_header_hash) = b_vec.remove(0); + // Removing corresponding finalized header data of popped slot + // as that data will not be used by relayer anyway. + >::remove(finalized_header_hash); + >::remove(finalized_header_hash); + >::remove(finalized_header_hash); } - b_vec.try_push(slot) + b_vec.try_push((slot, finalized_header_hash)) }) .map_err(|_| >::FinalizedBeaconHeaderSlotsExceeded)?; Ok(()) } - fn store_execution_header( + pub(super) fn store_execution_header( block_hash: H256, header: ExecutionHeader, beacon_slot: u64, @@ -922,7 +970,11 @@ pub mod pallet { ) { let block_number = header.block_number; + let (_, latest_mapping_to_insert) = Self::get_mapping_bound(); + >::insert(latest_mapping_to_insert, block_hash); + Self::update_mapping_bound(None, Some(latest_mapping_to_insert + 1)); >::insert(block_hash, header); + Self::prune_older_execution_headers(); log::trace!( target: "ethereum-beacon-client", @@ -941,6 +993,37 @@ pub mod pallet { Self::deposit_event(Event::ExecutionHeaderImported { block_hash, block_number }); } + fn get_mapping_bound() -> (u64, u64) { + >::get().unwrap_or((1, 1)) + } + + fn update_mapping_bound(oldest: Option, latest: Option) { + let (previous_oldest, previous_latest) = + >::get().unwrap_or((1, 1)); + let new_oldest = oldest.unwrap_or(previous_oldest); + let new_latest = latest.unwrap_or(previous_latest); + >::put((new_oldest, new_latest)); + } + + fn prune_older_execution_headers() { + let threshold = T::ExecutionHeadersPruneThreshold::get(); + let stored_execution_headers = >::count() as u64; + + if stored_execution_headers > threshold { + let (mut oldest_mapping_to_delete, _) = Self::get_mapping_bound(); + let execution_headers_to_delete = + stored_execution_headers.saturating_sub(threshold); + for _i in 0..execution_headers_to_delete { + let execution_header_hash = + >::get(oldest_mapping_to_delete); + >::remove(oldest_mapping_to_delete); + >::remove(execution_header_hash); + oldest_mapping_to_delete += 1; + } + Self::update_mapping_bound(Some(oldest_mapping_to_delete), None); + } + } + fn store_validators_root(validators_root: H256) { >::set(validators_root); } diff --git a/parachain/pallets/ethereum-beacon-client/src/mock.rs b/parachain/pallets/ethereum-beacon-client/src/mock.rs index ff2bc96380..9bd508a853 100644 --- a/parachain/pallets/ethereum-beacon-client/src/mock.rs +++ b/parachain/pallets/ethereum-beacon-client/src/mock.rs @@ -77,7 +77,9 @@ pub mod mock_minimal { pub const MaxPublicKeySize: u32 = config::PUBKEY_SIZE as u32; pub const MaxSignatureSize: u32 = config::SIGNATURE_SIZE as u32; pub const MaxSlotsPerHistoricalRoot: u64 = 64; - pub const MaxFinalizedHeaderSlotArray: u32 = 1000; + pub const MaxFinalizedHeaderSlotArray: u32 = 12; + pub const SyncCommitteePruneThreshold: u64 = 4; + pub const ExecutionHeadersPruneThreshold: u64 = 10; pub const WeakSubjectivityPeriodSeconds: u32 = 97200; pub const ChainForkVersions: ForkVersions = ForkVersions{ genesis: Fork { @@ -112,6 +114,8 @@ pub mod mock_minimal { type MaxSlotsPerHistoricalRoot = MaxSlotsPerHistoricalRoot; type MaxFinalizedHeaderSlotArray = MaxFinalizedHeaderSlotArray; type ForkVersions = ChainForkVersions; + type SyncCommitteePruneThreshold = SyncCommitteePruneThreshold; + type ExecutionHeadersPruneThreshold = ExecutionHeadersPruneThreshold; type WeakSubjectivityPeriodSeconds = WeakSubjectivityPeriodSeconds; type WeightInfo = (); } @@ -203,6 +207,8 @@ pub mod mock_mainnet { epoch: 162304, }, }; + pub const SyncCommitteePruneThreshold: u64 = 4; + pub const ExecutionHeadersPruneThreshold: u64 = 10; } impl ethereum_beacon_client::Config for Test { @@ -218,6 +224,8 @@ pub mod mock_mainnet { type MaxSlotsPerHistoricalRoot = MaxSlotsPerHistoricalRoot; type MaxFinalizedHeaderSlotArray = MaxFinalizedHeaderSlotArray; type ForkVersions = ChainForkVersions; + type SyncCommitteePruneThreshold = SyncCommitteePruneThreshold; + type ExecutionHeadersPruneThreshold = ExecutionHeadersPruneThreshold; type WeakSubjectivityPeriodSeconds = WeakSubjectivityPeriodSeconds; type WeightInfo = (); } diff --git a/parachain/pallets/ethereum-beacon-client/src/tests.rs b/parachain/pallets/ethereum-beacon-client/src/tests.rs index ebd07b8651..d5bdb82c9d 100644 --- a/parachain/pallets/ethereum-beacon-client/src/tests.rs +++ b/parachain/pallets/ethereum-beacon-client/src/tests.rs @@ -5,10 +5,11 @@ mod beacon_tests { merkleization::MerkleizationError, mock::*, ssz::{SSZExecutionPayloadHeader, SSZSyncAggregate}, - BeaconHeader, Error, PublicKey, + BeaconHeader, Error, ExecutionHeader, PublicKey, SyncCommittee, }; use frame_support::{assert_err, assert_ok}; use hex_literal::hex; + use rand::{thread_rng, Rng}; use snowbridge_beacon_primitives::{ExecutionPayloadHeader, SyncAggregate}; use sp_core::{H256, U256}; use ssz_rs::prelude::Vector; @@ -546,4 +547,156 @@ mod beacon_tests { let hash_root = merkleization::hash_tree_root(payload.unwrap()); assert_ok!(&hash_root); } + + #[test] + pub fn test_prune_finalized_header() { + new_tester::().execute_with(|| { + let max_finalized_slots = ::MaxFinalizedHeaderSlotArray::get().try_into().unwrap(); + + // Keeping track of to be deleted data + let amount_of_data_to_be_deleted = max_finalized_slots / 2; + let mut to_be_deleted_hash_list = vec![]; + let mut to_be_preserved_hash_list = vec![]; + for i in 0..max_finalized_slots { + let mut hash = H256::default(); + thread_rng().try_fill(&mut hash.0[..]).unwrap(); + + if i < amount_of_data_to_be_deleted { + to_be_deleted_hash_list.push(hash); + } else { + to_be_preserved_hash_list.push(hash); + } + + ethereum_beacon_client::FinalizedBeaconHeadersBlockRoot::::insert(hash, hash); + ethereum_beacon_client::FinalizedBeaconHeaders::::insert(hash, BeaconHeader::default()); + assert_ok!(mock_minimal::EthereumBeaconClient::add_finalized_header_slot(i, hash)); + + } + + // We first verify if the data corresponding to that hash is still there. + let slot_vec = ethereum_beacon_client::FinalizedBeaconHeaderSlots::::get(); + assert_eq!(slot_vec.len(), max_finalized_slots as usize); + for i in 0..(amount_of_data_to_be_deleted as usize) { + assert_eq!(slot_vec[i].0, i as u64); + assert_eq!(slot_vec[i].1, to_be_deleted_hash_list[i]); + + assert!(ethereum_beacon_client::FinalizedBeaconHeadersBlockRoot::::contains_key(to_be_deleted_hash_list[i])); + assert!(ethereum_beacon_client::FinalizedBeaconHeaders::::contains_key(to_be_deleted_hash_list[i])); + } + + // We insert `amount_of_hash_to_be_deleted` number of new finalized headers + for i in max_finalized_slots..(max_finalized_slots+ amount_of_data_to_be_deleted) { + let mut hash = H256::default(); + thread_rng().try_fill(&mut hash.0[..]).unwrap(); + ethereum_beacon_client::FinalizedBeaconHeadersBlockRoot::::insert(hash, hash); + ethereum_beacon_client::FinalizedBeaconHeaders::::insert(hash, BeaconHeader::default()); + assert_ok!(mock_minimal::EthereumBeaconClient::add_finalized_header_slot(i, hash)); + } + + // Now, previous hashes should be pruned and in array those elements are replaced by later elements + let slot_vec = ethereum_beacon_client::FinalizedBeaconHeaderSlots::::get(); + assert_eq!(slot_vec.len(), max_finalized_slots as usize); + for i in 0..(amount_of_data_to_be_deleted as usize) { + assert_eq!(slot_vec[i].0, (i as u64 + amount_of_data_to_be_deleted)); + assert_eq!(slot_vec[i].1, to_be_preserved_hash_list[i]); + + // Previous values should not exists + assert!(!ethereum_beacon_client::FinalizedBeaconHeadersBlockRoot::::contains_key(to_be_deleted_hash_list[i])); + assert!(!ethereum_beacon_client::FinalizedBeaconHeaders::::contains_key(to_be_deleted_hash_list[i])); + + // data that was preserved should exists + assert!(ethereum_beacon_client::FinalizedBeaconHeadersBlockRoot::::contains_key(to_be_preserved_hash_list[i])); + assert!(ethereum_beacon_client::FinalizedBeaconHeaders::::contains_key(to_be_preserved_hash_list[i])); + } + }); + } + + #[test] + pub fn test_prune_execution_headers() { + new_tester::().execute_with(|| { + let execution_header_prune_threshold = ::ExecutionHeadersPruneThreshold::get(); + let to_be_deleted = execution_header_prune_threshold / 2; + + let mut stored_hashes = vec![]; + + for i in 0..execution_header_prune_threshold { + let mut hash = H256::default(); + thread_rng().try_fill(&mut hash.0[..]).unwrap(); + mock_minimal::EthereumBeaconClient::store_execution_header( + hash, + ExecutionHeader::default(), + i, + hash + ); + stored_hashes.push(hash); + } + + // We should have stored everything until now + assert_eq!(ethereum_beacon_client::ExecutionHeaders::::count() as usize, stored_hashes.len()); + + // Let's push extra entries so that some of the previous entries are deleted. + for i in 0..to_be_deleted { + let mut hash = H256::default(); + thread_rng().try_fill(&mut hash.0[..]).unwrap(); + mock_minimal::EthereumBeaconClient::store_execution_header( + hash, + ExecutionHeader::default(), + i+execution_header_prune_threshold, + hash + ); + + stored_hashes.push(hash); + } + + // We should have only stored upto `execution_header_prune_threshold` + assert_eq!(ethereum_beacon_client::ExecutionHeaders::::count() as u64, execution_header_prune_threshold); + + // First `to_be_deleted` items must be deleted + for i in 0..to_be_deleted { + assert!(!ethereum_beacon_client::ExecutionHeaders::::contains_key(stored_hashes[i as usize])); + } + + // Other entries should be part of data + for i in to_be_deleted..(to_be_deleted+execution_header_prune_threshold) { + assert!(ethereum_beacon_client::ExecutionHeaders::::contains_key(stored_hashes[i as usize])); + } + }); + } + + #[test] + pub fn test_prune_sync_committee() { + new_tester::().execute_with(|| { + let sync_committee_prune_threshold = ::SyncCommitteePruneThreshold::get(); + let to_be_deleted = sync_committee_prune_threshold / 2; + let mut storing_periods = vec![]; + + for i in 0..sync_committee_prune_threshold { + mock_minimal::EthereumBeaconClient::store_sync_committee(i, SyncCommittee::default()); + storing_periods.push(i); + } + + // We should retain every sync committee till prune threshold + assert_eq!(ethereum_beacon_client::SyncCommittees::::count() as u64, sync_committee_prune_threshold); + + // Now, we try to insert more than threshold, this should make previous entries deleted + for i in 0..to_be_deleted { + mock_minimal::EthereumBeaconClient::store_sync_committee(i+sync_committee_prune_threshold, SyncCommittee::default()); + storing_periods.push(i+sync_committee_prune_threshold); + } + + // We should retain last prune threshold sync committee + assert_eq!(ethereum_beacon_client::SyncCommittees::::count() as u64, sync_committee_prune_threshold); + + // We verify that first periods of sync committees are not present now + for i in 0..to_be_deleted { + assert!(!ethereum_beacon_client::SyncCommittees::::contains_key(i)); + } + + // Rest of the sync committee should still exists + for i in to_be_deleted..(sync_committee_prune_threshold+to_be_deleted) { + assert!(ethereum_beacon_client::SyncCommittees::::contains_key(i)); + } + + }); + } } diff --git a/relayer/chain/parachain/writer.go b/relayer/chain/parachain/writer.go index 656ce2661c..d1ec9fd37e 100644 --- a/relayer/chain/parachain/writer.go +++ b/relayer/chain/parachain/writer.go @@ -231,7 +231,7 @@ func (wr *ParachainWriter) GetFinalizedSlots() ([]uint64, error) { return nil, fmt.Errorf("create storage key for basic channel nonces: %w", err) } - var slots []types.U64 + var slots []state.FinalizedHeaderSlots _, err = wr.conn.API().RPC.State.GetStorageLatest(key, &slots) if err != nil { return nil, fmt.Errorf("get storage for latest basic channel nonces (err): %w", err) @@ -239,7 +239,7 @@ func (wr *ParachainWriter) GetFinalizedSlots() ([]uint64, error) { result := []uint64{} for _, slot := range slots { - result = append(result, uint64(slot)) + result = append(result, slot.Slot) } return result, nil diff --git a/relayer/relays/beacon/state/state.go b/relayer/relays/beacon/state/state.go index ed8a3fb5b7..3e0d4478bc 100644 --- a/relayer/relays/beacon/state/state.go +++ b/relayer/relays/beacon/state/state.go @@ -16,3 +16,8 @@ type FinalizedHeader struct { BeaconSlot uint64 ImportTime uint64 } + +type FinalizedHeaderSlots struct { + Slot uint64 + FinalizedHeaderHash common.Hash +}