Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Free finalized Ethereum updates #159

Merged
merged 14 commits into from
Jul 31, 2024
1 change: 1 addition & 0 deletions .github/workflows/parachain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
-p snowbridge-ethereum
-p snowbridge-router-primitives
-p snowbridge-runtime-common
-p snowbridge-runtime-test-common
-p bridge-hub-rococo-runtime
-p bridge-hub-rococo-integration-tests
-p asset-hub-rococo-integration-tests
Expand Down
60 changes: 50 additions & 10 deletions bridges/snowbridge/pallets/ethereum-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ mod tests;
mod benchmarking;

use frame_support::{
dispatch::DispatchResult, pallet_prelude::OptionQuery, traits::Get, transactional,
dispatch::{DispatchResult, PostDispatchInfo},
pallet_prelude::OptionQuery,
traits::Get,
transactional,
};
use frame_system::ensure_signed;
use snowbridge_beacon_primitives::{
Expand Down Expand Up @@ -83,6 +86,8 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
#[pallet::constant]
type ForkVersions: Get<ForkVersions>;
#[pallet::constant]
claravanstaden marked this conversation as resolved.
Show resolved Hide resolved
type FreeHeadersInterval: Get<Option<u32>>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be optional? Can't we just make free headers the default?

type WeightInfo: WeightInfo;
}

Expand Down Expand Up @@ -205,11 +210,10 @@ pub mod pallet {
#[transactional]
/// Submits a new finalized beacon header update. The update may contain the next
/// sync committee.
pub fn submit(origin: OriginFor<T>, update: Box<Update>) -> DispatchResult {
pub fn submit(origin: OriginFor<T>, update: Box<Update>) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
Self::process_update(&update)?;
Ok(())
Self::process_update(&update)
}

/// Halt or resume all pallet operations. May only be called by root.
Expand Down Expand Up @@ -281,10 +285,9 @@ pub mod pallet {
Ok(())
}

pub(crate) fn process_update(update: &Update) -> DispatchResult {
pub(crate) fn process_update(update: &Update) -> DispatchResultWithPostInfo {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: Do you think this function is even necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol maybe not! I think its just to much the Ethereum consensus spec, which has a pattern of a "process" method, that does a "verify" and an "apply".

Self::verify_update(update)?;
Self::apply_update(update)?;
Ok(())
Self::apply_update(update)
}

/// References and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#validate_light_client_update>
Expand Down Expand Up @@ -433,11 +436,13 @@ pub mod pallet {
/// Reference and strictly follows <https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#apply_light_client_update
/// Applies a finalized beacon header update to the beacon client. If a next sync committee
/// is present in the update, verify the sync committee by converting it to a
/// SyncCommitteePrepared type. Stores the provided finalized header.
fn apply_update(update: &Update) -> DispatchResult {
/// SyncCommitteePrepared type. Stores the provided finalized header. Updates are free
/// if the certain conditions are met, least of which being a successful update.
fn apply_update(update: &Update) -> DispatchResultWithPostInfo {
let latest_finalized_state =
FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
.ok_or(Error::<T>::NotBootstrapped)?;
let mut sync_committee_updated = false;
if let Some(next_sync_committee_update) = &update.next_sync_committee_update {
let store_period = compute_period(latest_finalized_state.slot);
let update_finalized_period = compute_period(update.finalized_header.slot);
Expand All @@ -464,13 +469,24 @@ pub mod pallet {
Self::deposit_event(Event::SyncCommitteeUpdated {
period: update_finalized_period,
});
sync_committee_updated = true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sync_committee_updated flag seems redundant. Following the code, when sync_committee_updated=True, it follows that update.next_sync_committee_update == Some(...)

};

let pays_fee = Self::may_refund_call_fee(
latest_finalized_state.slot,
update.finalized_header.slot,
sync_committee_updated,
);
let actual_weight = match update.next_sync_committee_update {
None => T::WeightInfo::submit(),
Some(_) => T::WeightInfo::submit_with_sync_committee(),
};

if update.finalized_header.slot > latest_finalized_state.slot {
Self::store_finalized_header(update.finalized_header, update.block_roots_root)?;
}

Ok(())
Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee })
}

/// Computes the signing root for a given beacon header and domain. The hash tree root
Expand Down Expand Up @@ -641,5 +657,29 @@ pub mod pallet {
let signing_root = Self::compute_signing_root(header, domain)?;
Ok(signing_root)
}

/// Updates are free if the update is successful and the interval between the latest
/// finalized header in storage and the newly imported header is large enough. All
/// successful sync committee updates are free.
pub(super) fn may_refund_call_fee(
latest_slot: u64,
improved_by_slot: u64,
sync_committee_updated: bool,
) -> Pays {
// If the sync committee was successfully updated, the update may be free.
if sync_committee_updated {
return Pays::No;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effectively is just returning the parameter of sync_committee_updated of mapped to Pays, which seems redundant.

And given my other comment above, I would advise restructuring as follows:

fn check_refundable(update: &Update, latest_slot: u64) -> Pays


// If free headers are allowed and the latest finalized header is larger than the
// minimum slot interval, the header import transaction is free.
if let Some(free_headers_interval) = T::FreeHeadersInterval::get() {
if improved_by_slot >= latest_slot + free_headers_interval as u64 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is basically saying you get a free update if the update is far enough in the future?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, seems follow the same pattern for the P->K bridge here with free update at a minimal interval.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. It will be set to every 32 slots (so basically all the finalized updates), but if we ever need to make it less frequent we can.

return Pays::No;
}
}

Pays::Yes
}
}
}
3 changes: 2 additions & 1 deletion bridges/snowbridge/pallets/ethereum-client/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::{fs::File, path::PathBuf};
type Block = frame_system::mocking::MockBlock<Test>;
use frame_support::{
migrations::MultiStepMigrator,
traits::{OnFinalize, OnInitialize},
traits::{ConstU32, OnFinalize, OnInitialize},
};
use sp_runtime::BuildStorage;

Expand Down Expand Up @@ -110,6 +110,7 @@ parameter_types! {
impl ethereum_beacon_client::Config for Test {
type RuntimeEvent = RuntimeEvent;
type ForkVersions = ChainForkVersions;
type FreeHeadersInterval = ConstU32<96>;
type WeightInfo = ();
}

Expand Down
Loading
Loading