-
Notifications
You must be signed in to change notification settings - Fork 108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simplify beacon light client #839
Conversation
pub(super) type FinalizedBeaconHeaderStates<T: Config> = | ||
StorageValue<_, BoundedVec<FinalizedHeaderState, T::MaxFinalizedHeadersToKeep>, ValueQuery>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FinalizedHeaderState
used here is for pruning obsolete data in FinalizedBeaconState
. I would prefer we keep it.
snowbridge/parachain/pallets/ethereum-beacon-client/src/lib.rs
Lines 786 to 790 in 8f6ed31
let oldest = b_vec.remove(0); | |
// Removing corresponding finalized header data of popped slot | |
// as that data will not be used by relayer anyway. | |
<FinalizedBeaconHeadersBlockRoot<T>>::remove(oldest.beacon_block_root); | |
<FinalizedBeaconHeaders<T>>::remove(oldest.beacon_block_root); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved in #840
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, after thinking about the pruning algorithm some more, I don't think we should be using FinalizedBeaconHeaderStates
for pruning. That was a mistake, I believe.
FinalizedBeaconHeaderStates
was originally introduced to help the offchain relayer generate ancestry proofs for execution headers updates. The storage item only needed contain the last few dozen recently imported finalized header states.
Then for pruning, we decided to reuse FinalizedBeaconHeaderStates
for another purpose - to determine an older finalized header state we could prune. This was a mistake.
However this introduces a very bad constraint. If we want to keep the last months of beacon headers, then we need to store a month of state in FinalizedBeaconHeaderStates
. But this storage item is a vector. We can't keep so much data in a vector in storage. This will eventually cause weight problems.
So I want to us to eliminate FinalizedBeaconHeaderStates
. I don't think its necessary. I'm certain we can change the algorithm in the relayer to not rely on it.
And for pruning finalized beacon state, that's not a priority right now. We can introduce the Ring Buffer approach in another PR. But not right now - I want us to focus on these cleanups first. We can revisit pruning at a later time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that make sense. Just remove pruing stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to keep the last months of beacon headers
I think no need to cache that much data, based on the calculateClosestCheckpointSlot
algorithm here checkpoint only valid in range of 1 sync period(i.e. 27.3 hour). Consider finalized_update
changes very infrequently(i.e. almost 12mins) in mainnet, that means 27*60/12=135
checkpoints at most.
Another reason is since we've removed SyncCommittees
Map storage with only the current/next SyncCommittee. That means we can only verify updates in recent sync period, cache a lot of history headers does not make sense any more.
Any way, I agree we can make this PR focus on ALC spec compliant and revisit pruning at a later time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think no need to cache that much data, based on the calculateClosestCheckpointSlot algorithm here checkpoint only valid in range of 1 sync period(i.e. 27.3 hour). Consider finalized_update changes very infrequently(i.e. almost 12mins) in mainnet, that means 27*60/12=135 checkpoints at most.
If a finalized checkpoint is always the first slot in a finalized epoch, then I think it is not necessary for the relayer to need the FinalizedBeaconHeaderSlotsCache
storage . I've added more details in SNO-449.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome! 🙌
@@ -0,0 +1,43 @@ | |||
{ | |||
"header": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this shouldn't be beacon_header
, for clarity?
@@ -77,9 +73,25 @@ pub struct FinalizedHeaderUpdate<const COMMITTEE_SIZE: usize, const COMMITTEE_BI | |||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = "")) | |||
)] | |||
pub struct ExecutionHeaderUpdate { | |||
/// Header for the beacon block containing the execution payload |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I neglected comments like these, apologies. 😄
SyncCommitteeUpdate beaconjson.Update | ||
FinalizedHeaderUpdate beaconjson.Update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use only Update
in the relayer as well and remove one of these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need both. The relayer syncing algorithm hasn't changed at all. Only the data structures we send to the light client
HeaderUpdate beaconjson.HeaderUpdate | ||
} | ||
|
||
const ( | ||
pathToBeaconBenchmarkData = "parachain/pallets/ethereum-beacon-client/src/benchmarking" | ||
pathToBenchmarkDataTemplate = "parachain/templates/beacon_benchmarking_data.rs.mustache" | ||
pathToBenchmarkDataTemplate = "parachain/templates/benchmark-fixtures.mustache" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we shouldn't indicate that this is beacon data related?
if err != nil { | ||
return fmt.Errorf("write initial sync to file: %w", err) | ||
} | ||
} | ||
checkPointBytes, _ := types.EncodeToBytes(checkPointScale) | ||
// Call index for EthereumBeaconClient.force_checkpoint | ||
checkPointCallIndex := "0x3201" | ||
checkPointCallIndex := "0x3200" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call index for extrinsic force_checkpoint
, we use it for beacon client provisioning, replace the genesis config before.
// lastFinalizedHeader, err := h.writer.GetLastFinalizedHeaderState() | ||
// if err != nil { | ||
// return fmt.Errorf("fetch last finalized header from parachain: %w", err) | ||
// } | ||
// lastFinalizedHeaderPeriod := h.syncer.ComputeSyncPeriodAtSlot(lastFinalizedHeader.BeaconSlot) | ||
|
||
// Period + 1 because the sync committee update contains the next period's sync committee | ||
if lastSyncedSyncCommitteePeriod != period+1 { | ||
return fmt.Errorf("synced committee period %d not imported successfully", lastSyncedSyncCommitteePeriod) | ||
} | ||
// if lastFinalizedHeaderPeriod != period+1 { | ||
// return fmt.Errorf("synced committee period %d not imported successfully", lastFinalizedHeaderPeriod) | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove this? Is this because we have a combined header and sync committee period submit extrinsic now?
SignatureSlot uint64 `json:"signature_slot"` | ||
BlockRootsRoot string `json:"block_roots_root"` | ||
BlockRootBranch []string `json:"block_roots_branch"` | ||
type Update struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would the frequency of this update to the beacon client be? Trying to wrap my head around what the change means practically. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since SyncCommitteeUpdate
is just a FinalizedUpdate
with one extra field NextSyncCommittee
, we merge these two data types together In this PR and frequency is just same as the previous FinalizedUpdate
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't actually changed the syncing algorithm much at all. Only the data structures and the light client APIs the relayer talks to. The syncing algorithm used by the relayer remains compatible with the new design. Which is great.
When the relayer needs to submit a new sync committee, it sends this Update
data structure. And the same for when it only needs to submit a new finalized header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Strict follows ALC * Polish * Merge recent change * Fix tests * Polish * Generate all updates in same command * Sync version of lodestar * Clean code * Add more checks when generate test data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
I've refactored the light client to conform more closely to the ALC consensus specs.
The light client now has a single
submit
extrinsic for processing both finality and sync committee updates.Everything is green and running fine: Unit tests, benchmarks, relaying, E2E env
Storage
The storage in the light client is also simpler. These new storage items have replaced other ones:
It is no longer necessary to store historical sync committees, so I have removed that storage map.
Removed Features
I've decided to remove weak subjectivity checks from the scope of our initial release. I think the problem of dealing with long-range attacks has turned out to be quite a bit more complex than we anticipated. Then, given what we know now about the security flaws in the ALC protocol, long-range attacks are no longer the most pressing security vulnerability.
Since the current code in pallet was incomplete, and still needed to support governance-backed re-activation of the bridge, I felt it best to just remove it all for now.
Development Experience
Finally, the development experience is more straightforward. I've removed the
minimal
feature flag, and replaced it with abeacon-spec-mainnet
flag. So I've inverted the behavior, and this ends up actually working a lot better.When running unit tests, we usually want to runt the
minimal
spec tests, as those are extensive and easier to maintain and troubleshoot. So I've enabled the miminal spec tests by default, and you don't need to pass any feature flags to run them.However, when working with benchmarks, we always need to use the
mainnet
spec. So when theruntime-benchmarks
feature is enabled, thebeacon-spec-mainnet
is automatically enabled too.So can just run benchmark tests using:
Fixes: SNO-446, SNO-439