Skip to content
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

Fork-specific consensus-types interfaces #13948

Merged
merged 5 commits into from
May 3, 2024
Merged

Conversation

kasey
Copy link
Contributor

@kasey kasey commented May 2, 2024

What type of PR is this?

Other

What does this PR do? Why is it needed?

This brings back the changes from #13928 that got lost in the shuffle of various electra branches.

The goal of this PR is to deprecate the following patterns:

  • monolithic block body interface across all forks. This model requires all non-phase0 attributes getters to return an error that must be checked.
  • monolithic ExecutionData interface. In addition to the unsupported getter error checking issue, these types also require new getters to be backported to every previous fork.

In place of these patterns, we can use interface composition to add a new interface at each fork. The ExecutionData version for a particular fork can be accessed with a vanilla go type assertion. The BeaconBlockBody equivalent requires a little more wiring, because there is only one underlying concrete type which satisfies the interfaces of all forks. So we can add safe cast helpers to consensus-types/interfaces/cast.go.

@kasey kasey requested a review from a team as a code owner May 2, 2024 22:26
@kasey kasey requested review from terencechain, rkapka and nisdas May 2, 2024 22:26
terencechain
terencechain previously approved these changes May 2, 2024
WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error)
}

type ExecutionDataElectra interface {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't there be ExecutionDataCapella and ExecutionDataDeneb too? That will allow us to remove unnecessary methods from previous forks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an incremental step, but yes I would like us to add interfaces for each fork.

// the Electra hard fork. If the value is for an earlier fork (based on comparing its Version() to the electra version)
// an error will be returned. Callers that want to conditionally process electra data can check for this condition
// and safely ignore it like `if err != nil && errors.Is(interfaces.ErrInvalidCast) {`
func AsROBlockBodyElectra(in ReadOnlyBeaconBlockBody) (ROBlockBodyElectra, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as with execution data, shouldn't we have equivalent functions for all other forks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, once we backport to the other forks.

Comment on lines +948 to +950
epe, postElectra := data.(interfaces.ExecutionDataElectra)
if postElectra {
drs := epe.DepositReceipts()
Copy link
Contributor

Choose a reason for hiding this comment

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

This pattern assumes that every interfaces.ExecutionData in future forks, that also satisfies ExecutionDataElectra will have to be handled in the same way. Say for example that interfaces.ExecutionDataEpbs satisfies the Electra interface, it requires different handling of deposits but it requires the same handling of 7002 withdrawals, etc. If we explicitly change it to not satisfy ExecutionDataElectra and satisfy a different ExectutionDataEpbs then we would have to copy paste the withdrawal handling. If we keep it satisfying ExecutionDataElectra then we would have to deal with deposits with an if-else data.(interfaces.ExecutionDataEpbs) within this block right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy/paste from team chat:

If the ssz is the same, then it's fine for ExecutionDataEpbs to satisfy ExecutionDataElectra. Satisfying the interface only dictates what kind of value are available, it does not dictate how the data should be processed. That should always be based on the slot (or version, which is based on the slot).

SSZ changes in nested types is similar to what we just went through with Attestations. In that case our solution was to move attestations to an interface. A benefit of this kind of composition is that we would only need to implement the logic to deal with the interface in 2 places.If we do want to actually deprecate (remove) a field, then yes one option would be to break the chain of interface composition. Another would be to backport an error to that fields signature. Another would be to allow the getter to return a nil value and rely on the caller to be fork-aware. Another option is to rely less on these big interfaces and move processing functions to accept narrower interfaces. For example, epbs could take:

type EpbsWithdrawalGetter interface {
    GetWithdrawals() *eth.PBSWithdrawal
}

whereas other methods would take:

type WithdrawalsGetter interface {
    GetWithdrawals() []byte
}

and then perform assertions on those instead of the whole big type. A problem in both cases is that now if we want block to have something like an ExecutionData type that the container could use to abstractly carry the payload. This type would have to become /increasingly/ abstract (some kind of ConsensusType interface that would have ssz methods etc, which is asserted downstream to narrower interfaces). Otherwise we would also break the chain of composition in the container (block body).

TL;DR this approach handles all the previous fork changes pretty well, with attestations being the only notable exception. If we see more extreme changes across forks I think the best way to deal with that is probably more abstract interfaces for nested types, and narrow interfaces used by consumers of those fields. We'll need to keep iterating on it to see what feels most maintainable in practice.

@kasey kasey added this pull request to the merge queue May 3, 2024
Merged via the queue into develop with commit 80bff0d May 3, 2024
17 checks passed
@kasey kasey deleted the fork-specific-interfaces branch May 3, 2024 17:20
nisdas pushed a commit that referenced this pull request Jul 4, 2024
* fork-specific interface for electra

* add electra to wrapped payload switch

* use electra body in block factory

* deepsource

* rm pb getters from electra payload

---------

Co-authored-by: Kasey Kirkham <kasey@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants