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/GetAssembledBlockCurrentHeaderInsertBlocksValidateChainUpdateForkChoice
internal/el: separate execution-layer runtime and gRPC server exposingexecution.Executioninternal/engine: thin execution gRPC client adapter used by the consensus processinternal/consensus: validator set, qBFT round state, and the orchestration serviceinternal/builder: payload-to-block reconstruction and timestamp validationinternal/store: Pebble-backed durable stateinternal/p2p: libp2p host + gossipsub transport with mDNS/static peer bootstrapinternal/gen/sakhali/v1: protobuf code generated bybufinternal/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
- 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+1sizing, for example4or7 - block timestamps are treated as millisecond-precision values, so sub-second periods such as
500msare 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
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:9092Then 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.
Wire and persistence schemas live under proto/. Regenerate Go code with:
buf generateLocal deployment assets live under infrastructure/deployment.
The first packaged environment is a Docker-based localnet with:
4validator nodes1non-validator follower- one
sakhaliconsensus container and one execution-layer container per node
See infrastructure/deployment/README.md for the generator, Docker image build, and docker compose workflow.
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 like500ms--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 forValidateChain--forkchoice-timeout: timeout Erigon should spend on a forkchoice request before returning busy--p2p.topic: gossip topic for qBFT messages--p2p.discovery:mdns,static, ornone--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 mode0400or0600
Execution (cmd/sakhali-el):
--data-dir: execution-layer datadir--genesis: path to the genesis JSON file--grpc.addr: gRPC address exposingexecution.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
- 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.