At the start of every session, fetch the latest PoS team standards from:
<https://raw.githubusercontent.com/0xPolygon/pos-standards/main/team-standards.md>
These rules apply to every change in this repo unless this repo's
.claude/rules/ explicitly overrides them.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This guide provides comprehensive instructions for AI agents working on the Bor codebase. It covers the architecture, development workflows, and critical guidelines for effective contributions.
Bor is the execution client of Polygon PoS, forked from go-ethereum. It handles block production, transaction execution, and state management. Heimdall is the separate consensus client that manages validator selection, checkpointing to Ethereum, and span/sprint coordination. Together they form the complete Polygon PoS stack.
Bor focuses on high throughput, low gas fees, and full EVM compatibility.
Bor vs upstream go-ethereum: Bor is a fork of geth with significant modifications. Key differences:
- Consensus: Bor uses PoA (Proof of Authority) with sprint-based block production, not PoW or PoS beacon chain. The
consensus/bor/package is entirely Bor-specific. - Block production: Sprint/span model with validator rotation coordinated by Heimdall. Primary producer (succession 0) has priority; backups have increasing delays.
- State sync: L1→L2 state transfer via system contract calls in
Finalize, not via beacon chain deposits. - Parallel execution: BlockSTM (
core/blockstm/) for parallel transaction execution — not present in upstream geth. - Hard forks: Bor has its own fork schedule (
Delhi,Indore,Aalborg,Agra,Napoli,Bhilai,Rio) independent of Ethereum forks, though it also adopts some Ethereum forks. - Inherited but unused: Some geth packages (e.g.,
eth/catalyst/Engine API,consensus/ethash/) exist but are not used in Bor's production consensus.
- Bor Consensus (
consensus/bor/): Execution-side consensus logic; validates blocks using validator info from Heimdall, manages sprint-based block production - Core (
core/): Blockchain state management, transaction validation, and block processing - Storage (
ethdb/): Database abstraction layer supporting LevelDB and Pebble backends - Networking (
p2p/): P2P networking stack with peer discovery, sync, and transaction propagation - RPC (
rpc/): JSON-RPC server supporting HTTP, WebSocket, and IPC transports - Execution (
core/vm/): EVM implementation with BlockSTM parallel execution (core/blockstm/) - Sync (
eth/downloader/): Block synchronization with full and snap sync modes - Node (
node/): Node lifecycle management, service orchestration, and RPC stack - Tracing (
eth/tracers/): Transaction tracing with JS, native, and live tracers - CLI (
cmd/cli/,internal/cli/): Command-line interface with gRPC server for node management - TxPool (
core/txpool/): Transaction pool with subpools (legacy, blob) for pending transaction management - Stateless (
core/stateless/): Stateless execution engine with witness tracking and verification - WIT Protocol (
eth/protocols/wit/): Witness protocol for peer communication and witness data broadcasting
- Modularity: Each package can be used as a standalone library
- Performance: Goroutines for concurrency, efficient data structures, memory-mapped I/O
- Extensibility: Interfaces allow different implementations (consensus engines, databases)
- Go Idioms: Explicit error handling, small interfaces, composition over inheritance
-
Build: Build the main node binary
make bor
-
Format: Always format before committing
make fmt
-
Lint: Run golangci-lint
make lint-deps && make lint -
Test: Run tests before submitting
make test -
Docs: Regenerate CLI docs and default config
make docs
-
Unit Tests: Test individual functions
go test -v ./path/to/package -
Integration Tests: Test component interactions
go test -v -tags=integration ./tests/... -
Race Detection: Always run before submitting
go test -race ./... -
Benchmarks: For performance-critical code
go test -bench=. -benchmem ./path/to/package
- Avoid Allocations in Hot Paths: Use
sync.Pool, preallocate slices - Goroutines: Use for concurrent/parallel work, but don't spawn unbounded
- Channels: Use for coordination, prefer buffered for producers
- Context: Always propagate for cancellation
- Database: Use
ethdbinterfaces, batch writes when possible
Bor manages real user funds on Polygon PoS. Security is not optional.
Detailed, path-scoped security rules are in .claude/rules/:
| Rule File | Scope | What It Covers |
|---|---|---|
security-common.md |
All code | Severity classification, mandatory pre-commit checks, Go security patterns |
consensus-security.md |
consensus/, miner/ |
Validator auth, sprint logic, Heimdall trust, fork choice |
contract-interaction-security.md |
accounts/abi/, consensus/bor/contract/, consensus/bor/statefull/ |
ABI encoding, system contract calls, return value validation |
blockchain-security.md |
core/blockchain*.go, core/types/, core/rawdb/, rlp/, params/ |
Chain insertion, reorgs, fork choice, genesis, RLP, fork activation |
eth-backend-security.md |
eth/, ethdb/, ethstats/ |
Engine API, fetcher, filters, gas estimation, tracers, database |
crypto-security.md |
crypto/, keystore/, signer/ |
Key handling, constant-time ops, signature validation |
p2p-security.md |
p2p/, eth/protocols/, downloader/ |
Message bounds, peer DoS, eclipse attacks, sync safety |
rpc-security.md |
rpc/, node/, ethapi/, graphql/ |
Input validation, auth, resource limits, API exposure |
evm-security.md |
core/vm/ |
Gas accounting, opcode correctness, precompile safety, EIP gating |
txpool-security.md |
core/txpool/ |
Pool limits, validation ordering, eviction gaming, blob handling |
state-security.md |
core/state/, blockstm/, trie/ |
Parallel execution safety, state root determinism, journal correctness |
hardfork-rollout.md |
params/config.go, params/protocol_params.go, internal/cli/server/chains/*.go, builder/files/genesis-*.json, core/forkid/forkid.go, core/vm/, consensus/bor/bor.go, miner/worker.go |
Hardfork activation wiring, runtime/genesis parity, fork ID, EVM and miner gates |
These rules load automatically when Claude works on files matching their path patterns. Some files match multiple rules (e.g., consensus/bor/contract/ matches both consensus-security.md and contract-interaction-security.md) — all matching rules apply simultaneously.
This section intentionally summarizes .claude/rules/hardfork-rollout.md for
agents launched directly in this repo. Keep both surfaces in sync when changing
hardfork rollout guidance.
Even outside the PoS team workspace, treat every new or modified hardfork as
consensus-critical. A diff is hardfork-shaped if it adds or changes fork names,
fork block values, BorConfig fields, Is<Fork>(), params.Rules, fork ID
inputs, startup banners, chain config compatibility, EVM gas/opcode/precompile
behavior, header extra fields, producer timing, early block announcement, or
fork-gated miner behavior.
For every hardfork-shaped diff, explicitly compare the fork name and activation block across all runtime and packaged config surfaces:
params.BorMainnetChainConfigandparams.AmoyChainConfiginparams/config.gointernal/cli/server/chains/mainnet.goandinternal/cli/server/chains/amoy.gobuilder/files/genesis-mainnet-v1.jsonandbuilder/files/genesis-amoy.jsoncore/forkid/forkid.go,ChainConfig.Rules, startup banner output, and config compatibility / fork ordering checks
The specific failure class to prevent is a fork block present only in packaged
genesis JSON while normal bor server --config ... startup still loads a Go
runtime preset with Bor.<Fork>Block == nil. In that case
c.Bor.Is<Fork>(num) never activates on the normal startup path even though the
genesis file looks correct.
For VM hardforks, also compare the full active precompile set across the
previous fork, the new fork, and historical replay windows. The Pro hardfork
failure class is a new VM fork copying the wrong precompile set: P256 must be
carried forward from MadhugiriPro onward, KZG must remain available for
historical Madhugiri/MadhugiriPro/Lisovo blocks, and KZG must stay absent from
LisovoPro onward. On mainnet, Madhugiri and MadhugiriPro activated at the same
block, so only the MadhugiriPro boundary is externally observable there.
Changes touching core/vm/contracts.go, core/vm/jump_table.go, or
params.Rules should update or explicitly preserve the precompile continuity
matrix and boundary tests in core/vm/contracts_continuity_test.go. A new VM
fork must state the intended precompile delta versus the previous fork; silently
copying the old precompile map is a review finding unless the PR documents why
every special-case precompile is retained.
Fork-gated EVM, precompile, consensus, and miner behavior must be tested at
N-1, N, and N+1 for each network activation block. If the fork changes
consensus output, track Polygon Erigon parity before treating the rollout as
complete.
- Identify impact: What other components depend on this code?
- Plan implementation: Outline the approach before writing code
- Plan testing: How will you verify correctness? What edge cases exist?
- Check for breaking changes: Will this affect public APIs, configs, or stored data?
- Don't Block Goroutines: Avoid sync operations in async contexts
- Handle Errors: Never ignore errors with
_ - Close Resources: Channels, files, DB iterators, HTTP bodies
- Race Conditions: Use
-raceflag, protect shared state - Nil Checks: Check interface values and pointers before use
- Large, sweeping changes: Keep PRs focused and reviewable
- Mixing unrelated changes: One logical change per PR
- Ignoring CI failures: All checks must pass
- Incomplete implementations: Finish features before submitting
- Non-obvious behavior or edge cases
- Performance trade-offs and why a particular approach was chosen
- Constraints and assumptions that aren't obvious from the code
- Limitations or gotchas that future developers should know
- Why simpler alternatives don't work
// Sprint length is read from BorConfig.Sprint map (keyed by fork block).
// Never hardcode — use config.CalculateSprint(blockNumber) instead.
sprintLen := c.config.CalculateSprint(blockNumber)
// Fetch validator set at sprint start, not current block, to ensure
// all nodes agree on the producer for this sprint.
func (c *Bor) GetCurrentValidators(headerHash common.Hash, blockNumber uint64) ([]*Validator, error)
// IsSprintStart returns true if block is first in a sprint.
// Note: Block 0 is not a sprint start since there's no previous sprint to end.
func IsSprintStart(blockNumber uint64, sprintLength uint64) bool
// Map keyed by validator address for O(1) signer lookup during block verification
var signerCache = make(map[common.Address]*Validator)- Self-explanatory code - if the code is clear, don't add noise
- Restating code in English -
// increment counterabovecounter++ - Describing what changed - that belongs in commit messages, not code
Before adding a comment, ask: Would someone reading just the current code (no PR, no git history) find this helpful?
-
Logging: Use
logpackage with appropriate levelslog.Debug("Processing block", "number", block.Number(), "hash", block.Hash())
-
Metrics: Add prometheus metrics for monitoring
metrics.GetOrRegisterCounter("chain/inserts", nil).Inc(1)
-
Profiling: Use pprof for CPU/memory profiling
go tool pprof http://localhost:6060/debug/pprof/profile
Prefix with package name: eth, rpc: make trace configs optional
- All tests pass (
make test+make test-integration) - Linting passes (
make lint) - Code formatted (
make fmt)
- develop - Main development branch, PRs target here
- master - Stable release branch
Update CLAUDE.md when:
- Claude makes a mistake or wrong assumption → Add clarifying context
- New patterns or conventions are established → Document them
- Frequently asked questions arise → Add answers here
This file should evolve over time to capture project-specific knowledge that helps AI agents work more effectively.
PoS operator deployments are not all 1 Bor : 1 Heimdall. RPC providers in particular may run M:N Bor:Heimdall (multiple Bors fanned out behind one Heimdall, or vice versa), and some operators may run Heimdall without a connected Bor at all.
Treat "is there a Bor reachable from this Heimdall?" and "is there a specific Heimdall paired exclusively to this Bor?" as open questions, not invariants. Any feature that introduces a hard Heimdall→Bor dependency (or sticky Bor→Heimdall coupling) will break those topologies. Design new features so they degrade gracefully when the assumed peer isn't reachable.