Handling Generic Forks and Understanding Types in Lodestar

Explore Lodestar's guide to mastering types and forks. Learn key patterns and tools to simplify contributions and enhance your understanding of Ethereum's consensus clients.

Handling Generic Forks and Understanding Types in Lodestar

Welcome, new contributors!

Are you finding it challenging to navigate generic forks and understand how types work within the Lodestar Ethereum consensus client? You're not alone! In this blog post, we’ll break down these tricky topics and show you how to tackle them confidently. 

🌟
This guide is based on recent upgrades and aims to help you get started with contributing to our repository.

Understanding the Problem

The consensus client implementation follows strict specifications, defining all necessary data for a particular operation. During each fork (soft or hard), the specification updates these types or introduces new ones. If we have a type called BeaconBlock, introduced in the phase0 fork, future forks may change this type. Similarly, many other types are fork-dependent.

Following this vein, Lodestar includes a package called types, which has implementations of all types and exports those based on forks. You can use the following pattern to import fork-specific types.

While processing data, we can't always refer to data with a particular fork type, as it can belong to any fork. For that purpose, we had a unique namespace, allForks, to help us in these situations.

While this pattern helped us reach a certain point, the approach presented inherent problems. With the growing number of forks, these issues have only increased.

Here's a breakdown of the key issues:

  1. Limited Support of allForks namespace:
    • The allForks namespace has limited support and doesn't expose all the necessary types.
    • It doesn't allow filtering for a particular fork, making it cumbersome to get specific types.
  1. Redundancy and Duplication:
    • Using allForks led to redundant type definitions, which can cause code duplication across different packages.
    • Manually managing these types could be error-prone and requires remembering which forks apply to which types.
  1. Semantic Misalignment:
    • The use of allForks is semantically incorrect in many cases, implying that all types apply to all forks, which is untrue.
    • e.g. allForks.LightClientHeader refers to a type introduced with the light client protocol from Altair forward and does not exist in phase0.

Proposed Solutions

We examined the usage of types in our source code and observed that in most use cases, we don't want to address a type for allForks. Instead, we can identify a certain set of forks by identifying the common feature group. For example, rather than identifying the altair fork where it was introduced or addressing each individual fork, we use a single function, ForkLightClient, to manage light clients. 

Fork Groups

For that purpose, we introduce fork groups that can help address common forks.

These fork groups are unions from the ForkName enum and help address data with commonly known behaviour. But you can always fall back to ForkAll if you need help deciding which group to use.

Generic Types

The second part of the solution is to introduce generic-based types, which can accept individual forks or fork groups.

  1. The types here refer to the union of beacon block types from all the forks considered execution forks.
  2. The types here refer to the individual fork.
  3. The types here fall back to the default value ForkAll and refer to all forks, identical to previous allForks.BeaconBlock

With this approach, our types can be more flexible and friendlier for developers. Allowing access to individual types for a single fork or creating your own desired unions:

e.g. BeaconBlock<ForkName.phase0 | ForkName.bellatrix>.

Implementation strategy

To address these issues, we propose a phased approach:

Phase 1: Remove the allForks Namespace

  • Remove the allForks Namespace: Eliminate the allForks namespace entirely from our source code.
  • Introduce Generic Base Types: Create generic base types that allow us to pass fork names or collections of forks.

Phase 2: Consistent and Clean Type Interface

  • Expose Common Types: Only expose commonly used types like BeaconBlock, BeaconState, BeaconBlockHeader, etc., from the main namespace.
  • Discourage Namespace Use: Instead of using specific fork namespaces, use a utility type called TypesFor to pass collections of forks or individual forks.
  • SSZ Types: Ensure SSZ types have a consistent interface using a utility type similar to SSZTypesFor. To get actual ssz objects, you can use a utility function called sszTypesFor. Pay attention to the upper case SSZ and lower case ssz to distinguish between a type and a function.

Phase 3: Convert Namespaces to Types

Currently, Typescript does not allow you to map or iterate the namespaces, forcing us to duplicate some types of code.

  • Namespace to Type Conversion: Convert namespaces into types to leverage TypeScript's capabilities fully.
  • Remove Boilerplate Code: This change will help remove redundant boilerplate code that builds up over time.

Getting Started

To help new contributors, we've created detailed guidelines on how to add new types and handle forks:

  1. Add New Fork:
    • Define new fork names in the lodestar/params package under ForkName enum.
    • Update fork groups to include a new fork.
  1. Add New Types:
    • Define a new type in the specific fork it belongs to under lodestar/packages/types/src/[fork]/types.ts.
    • Import the type in lodestar/packages/types/src/types.ts and create a generic version of it. This step is necessary if different forks commonly use the type.
    • That generic should accept only one parameter, such as fork names.
    • Limit the support for fork by limiting with F extends <Supported Forks> for that type.

Resources for New Contributors

Conclusion

Understanding how to handle generic forks and the importance of types in Lodestar is essential for maintaining a robust and flexible Ethereum consensus client. We hope this guide helps you get started and contributes effectively to your project.

Contribute to Lodestar! 🌟

At ChainSafe, we constantly seek exceptional talent to join our teams and welcome contributors to the Lodestar consensus client. To any TypeScript developers looking to take on challenges and push the boundaries of the JavaScript ecosystem, get in touch! To get involved, visit our GitHub repository.

If you wish to contact the team, join Chainsafe's Discord in the #lodestar-general channel or explore our job openings page. Alternatively, you can email us at info@chainsafe.io.


Lodestar is built and maintained by ChainSafe.

Website | Youtube | Medium | Twitter | Linkedin | GitHub