Skip to content

Repository files navigation

Sakhali

Sakhali is a minimal external consensus client for an EVM chain backed by a separate execution-layer process.

The current shape is a simple PoS-flavored qBFT client:

  • a static validator set defines who can propose and vote
  • the proposer schedule is round-robin over that validator set by height, round
  • the proposer assembles an execution payload from Erigon; every validator reconstructs and execution-validates it before PREPARE
  • qBFT prepared, round-change, and commit certificates carry portable quorum evidence
  • validator and non-validator nodes advance canonical fork choice only from certified decisions
  • permissioned libp2p GossipSub carries protobuf consensus messages, and authenticated libp2p streams carry certified sync records

Erigon internals still provide the execution engine. Sakhali uses the execution-facing operations an external consensus layer needs:

  • AssembleBlock / GetAssembledBlock
  • CurrentHeader
  • InsertBlocks
  • ValidateChain
  • UpdateForkChoice

Components

  • internal/el: separate execution-layer runtime and gRPC server exposing execution.Execution
  • internal/engine: thin execution gRPC client adapter used by the consensus process
  • internal/consensus: validator set, qBFT round state, and the orchestration service
  • internal/builder: payload-to-block reconstruction and timestamp validation
  • internal/store: Pebble-backed durable state
  • internal/p2p: libp2p host + gossipsub transport with mDNS/static peer bootstrap
  • internal/gen/sakhali/v1: protobuf code generated by buf
  • internal/app: process configuration and flag parsing

The responsibilities are intentionally separated:

  • the engine adapter only talks to Erigon
  • the builder only turns an execution payload into a local candidate block and checks that it matches the expected parent/number/timestamp window
  • qBFT reasons about proposer validity, votes, round changes, quorum, and commit decisions
  • the service coordinates block production, durable vote state, sync catch-up, insertion, validation, and forkchoice updates
  • transport signs every qBFT protobuf message explicitly and verifies that signature against the pubsub author on receipt
  • the consensus service independently re-verifies sender membership, protocol domain, and application signature before mutating state

Current Assumptions

  • development may expose execution gRPC on loopback; production requires TLS 1.3 mutual authentication on a private workload network
  • validator identities are libp2p peer IDs configured in proposer order
  • validator count satisfies qBFT's 3f+1 sizing, for example 4 or 7
  • block timestamps are treated as millisecond-precision values, so sub-second periods such as 500ms are allowed
  • consensus state is persisted locally in Pebble and reloaded on restart
  • every validator execution-validates a reconstructed candidate without canonical fork-choice mutation before PREPARE

Quick Start

Start the execution-layer node:

go run ./cmd/sakhali-el \
  --data-dir data/execution \
  --genesis infrastructure/deployment/generated/localnet/genesis.json \
  --grpc.addr 127.0.0.1:9092

Then start the consensus node:

go run ./cmd/sakhali \
  --execution.addr 127.0.0.1:9092 \
  --consensus.block-period 500ms \
  --consensus.chain-id 13371337 \
  --consensus.genesis-hash <64-hex-genesis-hash> \
  --consensus.validators <peer-id-1>,<peer-id-2>,<peer-id-3>,<peer-id-4> \
  --p2p.private-key-file <path-to-hex-encoded-libp2p-private-key>

Use the CONSENSUS_GENESIS_HASH value emitted in the generated node env files; all validators must use the same chain ID, genesis hash, and ordered validator set.

The EL process boots the execution runtime, serves app-facing JSON-RPC and websocket APIs, and exposes the execution.Execution gRPC service on --grpc.addr. The consensus process waits for that gRPC service to report ready, then boots libp2p and starts proposing or validating blocks depending on whether its peer ID is in the validator set.

Protobuf

Wire and persistence schemas live under proto/. Regenerate Go code with:

buf generate

Deployment

Local deployment assets live under infrastructure/deployment.

The first packaged environment is a Docker-based localnet with:

  • 4 validator nodes
  • 1 non-validator follower
  • one sakhali consensus container and one execution-layer container per node

See infrastructure/deployment/README.md for the generator, Docker image build, and docker compose workflow.

Important Flags

Consensus (cmd/sakhali):

  • --execution.addr: Erigon execution gRPC address
  • --consensus.validators: ordered validator peer IDs used for proposer rotation and vote quorum
  • --consensus.block-period: minimum block spacing used when proposing new payloads, including sub-second values like 500ms
  • --consensus.round-timeout: base timeout before broadcasting a round change
  • --consensus.round-timeout-step: extra timeout added per higher round
  • --consensus.state-dir: Pebble directory for durable consensus state
  • --consensus.sync-batch-size: number of committed blocks sent per streamed sync batch
  • --consensus.fee-recipient: fee recipient for locally assembled payloads
  • --poll-interval: retry interval for gRPC busy responses
  • --ready-poll-interval: retry interval while waiting for Erigon to become ready
  • --validation-timeout: timeout for ValidateChain
  • --forkchoice-timeout: timeout Erigon should spend on a forkchoice request before returning busy
  • --p2p.topic: gossip topic for qBFT messages
  • --p2p.discovery: mdns, static, or none
  • --p2p.listen-addrs: comma-separated libp2p listen multiaddrs
  • --p2p.static-peers: comma-separated static peers to dial on startup
  • --p2p.private-key-file: path to a hex-encoded private key file for a stable peer ID; the file must have mode 0400 or 0600

Execution (cmd/sakhali-el):

  • --data-dir: execution-layer datadir
  • --genesis: path to the genesis JSON file
  • --grpc.addr: gRPC address exposing execution.Execution
  • --http.addr, --http.port, --http.api: public JSON-RPC listener settings
  • --ws, --ws.port: websocket RPC settings
  • --authrpc.addr, --authrpc.port, --authrpc.jwtsecret: authenticated Engine API settings
  • --p2p.listen-addr, --p2p.max-peers, --p2p.max-pending-peers, --p2p.no-discovery: execution-layer devp2p settings

Operational Notes

  • qBFT runs as proposal -> prepare -> commit, with certificate-backed timeout-driven round changes
  • proposing is event-driven: the next proposer is scheduled immediately after finalization or round advancement, and only waits for the block timestamp window when necessary
  • the proposal digest is the reconstructed execution payload block hash
  • consensus and sync messages are protobuf, not JSON
  • candidates may be imported non-canonically for pre-vote validation; only a valid quorum COMMIT certificate can advance canonical fork choice
  • committed records include their protocol domain, payload, and COMMIT certificate and are served through an authorized, bounded libp2p sync stream
  • restart recovery reconciles any decision journal, exhausts certified local replay, then performs explicit-target certified peer backfill before readiness
  • production validator identity comes from a permission-checked key file; private key material is not accepted through CLI values or emitted in generated metadata
  • production uses static allowlisted peers, mTLS execution control, private admin/observability surfaces, and hardened containers; local mDNS remains development-only

See docs/ARCHITECTURE.md for protocol and trust boundaries, docs/SECURITY-AUDIT-2026-07-26.md for the audit/remediation record, and docs/FUTURE-WORK-SECURITY-AND-PROTOCOL.md for accepted residual risks.

About

Custom chain based on qBFT consensus

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages