This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Charon is a distributed validator middleware client for Ethereum staking that enables running a single validator across a group of independent nodes using threshold BLS signatures. The project is written in Go and implements a sophisticated workflow architecture for coordinating validator duties across multiple nodes in a cluster.
# Build the charon binary
make charon
# Or manually
go build -trimpath -ldflags="-buildid= -s -w -X github.com/obolnetwork/charon/app/version.version=$(bash charon_version.sh)"# Run all tests
go test ./...
# Run tests with race detection (used in pre-commit hooks)
go test -failfast -race -timeout=2m ./...
# Run tests for specific package
go test ./core/scheduler
# Run a single test function
go test -run TestFunctionName ./path/to/package
# Run tests with verbose output
go test -v ./...# Run linter (as per .golangci.yml config)
golangci-lint run
# Format code
go fmt ./...
gofumpt -w .
# Fix imports
fiximports
# Run all pre-commit hooks manually
pre-commit run --all-filesDevelopment tools are defined in go.mod and can be installed via:
go install tool # e.g., go install github.com/bufbuild/buf/cmd/buf- See README.md for high-level overview
- See docs/architecture.md for detailed architecture documentation
- See docs/goguidelines.md for Go coding guidelines
- See docs/configuration.md for configuration options
- See docs/dkg.md for Distributed Key Generation details
- See docs/consensus.md for consensus design details
- See docs/metrics.md for metrics documentation
- See docs/reasons.md for error reason codes
- See docs/structure.md for package structure overview
- See docs/contributing.md for contribution guidelines
- See public product documentation
The heart of Charon is the core workflow, which processes validator duties through a series of components. Each duty (attestation, block proposal, etc.) flows through these stages:
- Scheduler → Triggers duties at optimal times based on beacon chain state
- Fetcher → Fetches unsigned duty data from beacon node
- Consensus → Uses QBFT (Istanbul BFT) to agree on duty data across all nodes
- DutyDB → Persists agreed-upon unsigned data and acts as slashing protection
- ValidatorAPI → Serves data to validator clients and receives partial signatures
- ParSigDB → Stores partial threshold BLS signatures from local and remote VCs
- ParSigEx → Exchanges partial signatures with peers via libp2p
- SigAgg → Aggregates partial signatures when threshold is reached
- AggSigDB → Persists aggregated signatures
- Bcast → Broadcasts final aggregated signatures to beacon node
Additional supporting components:
- Tracker → Tracks duty lifecycle events and records failure reasons
- Priority → Implements peer-aware priority ordering for consensus proposals
- Duty: Unit of work (slot + duty type). Cluster-level, not per-validator.
- PubKey: DV root public key, the identifier for a validator in the workflow
- UnsignedData: Abstract type for attestation data, blocks, etc.
- SignedData: Fully signed duty data
- ParSignedData: Partially signed data from a single threshold BLS share
- Immutable values flowing between components: Components consume and produce immutable values (like actors)
- Callback subscriptions: Components are decoupled via subscriptions rather than direct calls
- Type-safe encoding: Abstract types are encoded/decoded via dedicated files: core/unsigneddata.go, core/signeddata.go, core/eth2signeddata.go, core/ssz.go, core/proto.go
Charon uses QBFT (implementation of Istanbul BFT) for consensus. See core/qbft/README.md. Each duty requires consensus to ensure all nodes sign identical data (required for BLS threshold signatures and slashing protection).
app/ # Application entrypoint, wiring, infrastructure libraries (log, errors, tracer, lifecycle)
cluster/ # Cluster config, lock files, DKG artifacts
cmd/ # CLI commands (run, dkg, create, test, etc.)
core/ # Core workflow business logic and component implementations
dkg/ # Distributed Key Generation logic
eth2util/ # ETH2 utilities (signing, deposits, keystores)
p2p/ # libp2p networking and discv5 peer discovery
scripts/ # Build and development scripts
tbls/ # Threshold BLS signature scheme
testutil/ # Test utilities, mocks, golden files
- Uses forked
github.com/ObolNetwork/kryptology(security fixes) - Uses forked
github.com/ObolNetwork/go-eth2-client(kept up to date with upstream)
Requires Go 1.26 (enforced by pre-commit hooks)
Core Principles (from docs/goguidelines.md)
- Functions over methods: Prefer stateless functions over stateful objects
- Values over types: Prefer immutable structs over mutable objects
- Explicit over implicit: Don't hide behavior
- Unexported over exported: Write shy code, minimize public surface area
- Just return errors, avoid logging and returning
- Wrap external library errors for stack traces
- Use concise error messages:
errors.Wrap(err, "do something")not"failed to do something" - Use
app/errorspackage for structured errors with fields
- Maximize signal-to-noise ratio - keep logs scannable
- Levels:
error(critical, human intervention),warn(important failures),info(high-level outcomes),debug(important steps) - No
tracelevel - only usedebugfor tracing, mark with TODOs to remove - Keep messages concise and glanceable
- Use snake_case for log fields
- Test files use
_test.gosuffix - Internal tests use
_internal_test.go(package name with_testsuffix not used) - Pre-commit hook runs:
go test -failfast -race -timeout=2mon touched packages - Use
testutil/packages for mocks, golden files, etc.
- Data labels (json, logs, metrics):
snake_case - Package names: concise single or double nouns (e.g.,
scheduler,validatorapi) - Variable names: short and clear (
errfor errors, noterror)
When modifying core workflow components:
- Entry point:
main.go→app/app.go:Run()→wireCoreWorkflow()assembles all components - Check docs/architecture.md for component interfaces and data flow
- Ensure immutability - call
.Clone()before sharing/caching values - Update component subscriptions in the stitching logic if interfaces change
- Component implementations are in
core/<component>/directories
New duty types must be added to:
core/types.go: Add duty type constantcore/unsigneddata.go/core/signeddata.go: Add encoding/decoding logic- Scheduler, Fetcher, and other relevant components
cluster-definition.json: Intended cluster config (operators, validators)cluster-lock.json: Extends definition with DV public keys and shares (output of DKG)- See cluster/ package
Follow Go team's format: package[/path]: concise overview of change
- Examples:
core/scheduler: add sync committee support,app/log: improve structured logging
Description of the change in present tense.
category: <refactor|bug|feature|docs|release|tidy|fixbuild>
ticket: <#123 or none>
feature_flag: <optional, from app/featureset>
- PRs are always squash merged to main
- PR title and body become the commit message
- Only obol-bulldozer bot can merge (add
merge when readylabel after approval) - Multiple PRs per issue are encouraged (micro-commits on stable trunk)
- Compatible: Same MAJOR version, different MINOR/PATCH
- Incompatible: Different MAJOR version
- DKG: Requires matching MAJOR and MINOR versions (PATCH can differ)
- Feature flags are managed in
app/featureset/— use them to gate new behavior behind flags - No
tracelogs - usedebugwith TODOs for temporary tracing - Review and clean up logs periodically
- Prefer functions returning functions over creating new types with methods
- Always verify code doesn't contain security issues before committing
- Use pre-commit hooks (
pre-commit install) for fast local feedback