- Target branch:
unstable(neverstable) - Pre-push: run
pnpm lint,pnpm check-types,pnpm test:unitbefore every push - Relative imports: use
.jsextension in TypeScript ESM imports - No
any: avoidany/as any; use proper types or justifiedbiome-ignore - No
lib/edits: never editpackages/*/lib/— these are build outputs - Follow existing patterns before introducing new abstractions
- Structured logging with specific error codes (not generic
Error) - Incremental commits after review starts — do not force push unless maintainer requests it
Lodestar is a TypeScript implementation of the Ethereum consensus client (beacon node and validator client). It is maintained by ChainSafe Systems and serves as:
- Production beacon node for Ethereum's proof-of-stake consensus layer
- Validator client for stakers running validators
- Light client implementation with browser support
- Reference implementation for TypeScript/JavaScript ecosystem
/packages/
api/ # REST API client and server
beacon-node/ # Beacon chain node implementation
cli/ # Command-line interface
config/ # Network configuration (mainnet, sepolia, etc.)
db/ # Database abstraction (LevelDB)
era/ # Era file handling for historical data
flare/ # CLI debugging/testing tool
fork-choice/ # Fork choice implementation (proto-array)
light-client/ # Light client implementation
logger/ # Logging utilities
params/ # Consensus parameters and presets
prover/ # Execution API prover
reqresp/ # libp2p request/response protocol
spec-test-util/ # Test harness for consensus spec tests
state-transition/ # State transition functions
test-utils/ # Shared utilities for testing
types/ # SSZ type definitions
utils/ # Shared utilities
validator/ # Validator client
/configs/ # Network configuration files
/docs/ # Documentation source
/scripts/ # Build and release scripts
/dashboards/ # Grafana dashboard JSON files
All commands use pnpm as the package manager.
# Install dependencies
corepack enable
pnpm install
# Build all packages
pnpm build
# Build a specific package (faster iteration)
pnpm --filter @lodestar/beacon-node build
# Run linter (biome)
pnpm lint
# Fix lint issues automatically
pnpm lint:fix
# Type check all packages
pnpm check-types
# Type check a specific package
pnpm --filter @lodestar/beacon-node check-types
# Run unit tests (fast, minimal preset)
pnpm test:unit
# Run specific test file with project filter
pnpm vitest run --project unit test/unit/path/to/test.test.ts
# Run tests matching a pattern
pnpm vitest run --project unit -t "pattern"
# Run spec tests (requires downloading first)
pnpm download-spec-tests
pnpm test:spec
# Run e2e tests (requires docker environment)
./scripts/run_e2e_env.sh start
pnpm test:e2eTip: For faster iteration, run tests from the specific package directory:
cd packages/beacon-node
pnpm vitest run test/unit/chain/validation/block.test.tsLodestar uses Biome for linting and formatting.
- ES modules: All code uses ES module syntax (
import/export) - Naming:
camelCasefor functions/variables,PascalCasefor classes,UPPER_SNAKE_CASEfor constants - Quotes: Use double quotes (
") not single quotes - Types: Prefer explicit types on public APIs and complex functions
- No
anyoras any: Do not useanytype oras anyassertions to bypass the type system. In production code, find the proper type or interface. In test code, use public APIs rather than accessing private fields viaas any. If genuinely unavoidable, add a suppression with the full rule ID and justification:// biome-ignore lint/suspicious/noExplicitAny: <reason> - Private fields: No underscore prefix (use
private dirty, notprivate _dirty) - Named exports only: No default exports
Imports are auto-sorted by Biome in this order:
- Node.js/Bun built-ins
- External packages
@chainsafe/*and@lodestar/*packages- Relative paths
In TypeScript source and test files, use .js extension for relative ESM imports
(even though source files are .ts). This is required for Node.js ESM resolution.
This rule does not apply to non-TS files (e.g., package.json, .mjs config).
// ✅ Correct
import {something} from "./utils.js";
import {IBeaconStateView} from "../stateView/interface.js";
// ❌ Wrong — will break at runtime
import {something} from "./utils.ts";- Use
//for implementation comments - Use
/** */JSDoc format for documenting public APIs - Add comments when code behavior is non-obvious or deviates from standards
- Whitespace helps readability in complex code
Metrics are critical for production monitoring:
- Follow Prometheus naming conventions
- Always suffix metric names with units:
_seconds,_bytes,_total - Do NOT suffix code variables with units (no
Secsuffix) - Time-based metrics must use seconds
Code that varies by fork uses fork guards and type narrowing:
import {isForkPostElectra, isForkPostFulu} from "@lodestar/params";
// Check fork before accessing fork-specific fields
if (isForkPostElectra(fork)) {
// electra and later forks
}The fork progression is: phase0 → altair → bellatrix → capella →
deneb → electra → fulu → gloas.
ChainForkConfig combines base chain config with computed fork information:
// Access config values
config.SLOTS_PER_EPOCH; // from params
config.getForkName(slot); // computed fork for a slot
config.getForkTypes(fork); // SSZ types for a fork@lodestar/params holds constants (SLOTS_PER_EPOCH, etc.).
@lodestar/config holds runtime chain configuration.
- Get current state via
chain.getHeadState()— returns a tree-backed state - Never hold references to old states — they consume memory and can go stale
- For read-only access, use the state directly; for mutations, use
state.clone() - Beacon state is tree-backed (persistent data structure), making cloning cheap
Types use @chainsafe/ssz and come in two forms:
- Value types: Plain JS objects. Easy to work with, higher memory usage.
- View/ViewDU types: Tree-backed. Memory-efficient, used for beacon state.
// Type definition
const MyContainer = new ContainerType(
{
field1: UintNum64,
field2: Root,
},
{typeName: "MyContainer"}
);
// Value usage
const value = MyContainer.defaultValue();
value.field1 = 42;
// View usage (tree-backed)
const view = MyContainer.toViewDU(value);
view.field1 = 42;
view.commit();The fork choice store uses proto-array for efficient head computation:
getHead()returns a cachedProtoBlock— may be stale after mutations- After modifying proto-array node state (e.g., execution status), call
recomputeForkChoiceHead()to refresh the cache - This applies to any code that modifies proto-array outside normal block import
Use structured logging with metadata objects:
this.logger.debug("Processing block", {slot, root: toRootHex(root)});
this.logger.warn("Peer disconnected", {peerId: peer.toString(), reason});- Prefer structured fields over string concatenation
- Use appropriate levels:
error>warn>info>verbose>debug>trace - Include relevant context (slot, root, peer) as structured fields
Tests live alongside source code in test/ directories:
packages/beacon-node/
src/
test/
unit/ # Unit tests
e2e/ # End-to-end tests
perf/ # Performance benchmarks
spec/ # Consensus spec tests
- Tests must be deterministic (no external live resources)
- Do not pull from external APIs (run local nodes instead)
- Use pinned Docker tags and git commits (not branches)
- Add assertion messages for loops or repeated assertions:
for (const block of blocks) {
expect(block.status).equals("processed", `wrong status for block ${block.slot}`);
}See Build commands above for all test invocations. Use --project unit
for targeted runs and LODESTAR_PRESET=minimal for faster spec tests.
If contributing from the main repository:
username/short-description
Follow Conventional Commits:
feat:new featuresfix:bug fixesrefactor:code changes that don't add features or fix bugsperf:performance improvementstest:adding or updating testschore:maintenance tasksdocs:documentation changes
Examples:
feat: add lodestar prover for execution api
fix: ignore known block in publish blinded block flow
refactor(reqresp)!: support byte based handlers
Required: Disclose any AI assistance in your PR description:
> This PR was written primarily by Claude Code.
> I consulted Claude Code to understand the codebase, but the solution
> was fully authored manually by myself.
- Keep PRs as drafts until ready for review
- Avoid force push after review starts unless a maintainer requests it (use incremental commits)
- Flag stale PRs to maintainers rather than letting them sit indefinitely
- Respond to review feedback promptly — reply to every comment, including bot reviewers
- When updating based on feedback, respond in-thread to acknowledge
Before pushing any commit, verify:
pnpm lint— Biome enforces formatting; CI catches failures but wastes a round-trippnpm check-types— catch type errors before CIpnpm docs:lint— if you edited any.mdfiles, check Prettier formatting- No edits in
packages/*/lib/— these are build outputs; editsrc/instead
- Create a feature branch from
unstable - Implement the feature with tests
- Run
pnpm lintandpnpm check-types - Run
pnpm test:unitto verify tests pass - Open PR with clear description and any AI disclosure
- Write a failing test that reproduces the bug
- Fix the bug
- Verify the test passes
- Run checks:
pnpm lint,pnpm check-types,pnpm test:unit
- Add the type definition in the relevant fork file (e.g.,
packages/types/src/phase0/sszTypes.ts) - Export the new type from that file's
sszobject - The type will be automatically aggregated (no central
sszTypesto modify) - Run
pnpm check-typesto verify
- Define the route in
packages/api/src/beacon/routes/<resource>.ts - Add request/response SSZ codecs alongside the route definition
- Implement the server handler in
packages/beacon-node/src/api/impl/beacon/<resource>.ts - Add tests for the new endpoint
- Reference the Beacon APIs spec for the endpoint contract
- Prefer inline logic over single-use helper functions for simple checks
- Match existing patterns in the file you're modifying (comments, structure)
- Use specific error codes (
BlockErrorCode.PARENT_UNKNOWN) over genericError - Handle undefined explicitly:
config.directPeers ?? [],value?.trim() ?? ""
The primary reference for implementing consensus specs is the Ethereum consensus-specs repository. Additionally, eth2book.info is a valuable resource for understanding phase0, altair, bellatrix, and capella specs and how the spec evolved over time (though no longer actively maintained).
When implementing changes from the consensus specs, the mapping is typically:
| Spec Document | Lodestar Package |
|---|---|
| beacon-chain.md (containers) | @lodestar/types |
| beacon-chain.md (functions) | @lodestar/state-transition |
| p2p-interface.md | @lodestar/beacon-node (networking, gossip) |
| validator.md | @lodestar/validator |
| fork-choice.md | @lodestar/fork-choice |
Forks follow the progression defined in Architecture patterns > Fork-aware code above.
- @lodestar/types/src/ - Each fork has its own directory with SSZ type definitions
- @lodestar/state-transition/src/block/ - Block processing functions
(e.g.,
processAttestations,processDeposit,processWithdrawals) - @lodestar/state-transition/src/epoch/ - Epoch processing functions
- @lodestar/state-transition/src/slot/ - Slot processing functions
The specrefs/ directory contains pinned consensus spec versions.
When implementing spec changes, reference the exact spec version.