/** * consensus/byzantine.ts — In-process PBFT-style consensus for Bizar. * * F-039 — thin port of ruflo's `v3/@claude-flow/swarm/src/consensus/byzantine.ts`. * Bizar's review/decision steps need a 3-of-5 majority with view-change * on faulty proposers, but not the full PBFT transport stack. This * class is the in-memory consensus surface; it tracks one proposal * at a time per (cluster, quorum) tuple, and exposes the four PBFT * phases (`pre-prepare → prepare → commit → reply`) plus `view-change`. * * Design choices: * * • No network transport. Peers are passed in the constructor; the * class is a single-node simulator that an outer orchestrator * (or the MCP tool) drives by calling `onPrePrepare` / `onPrepare` * with each peer's vote. This is exactly the surface Bizar needs * for review-gate decisions; a real transport lives in a separate * concern (would be `transport.ts` in ruflo — not ported). * * • Determinism. Quorum math is purely set-based. The only ordering * input is the round-robin proposer election in `queen.ts`, and * even that accepts an explicit `proposerSeed` so tests can pin * the head. * * • Replay protection. `propose(payload)` is keyed on the payload's * stable hash. Re-submitting the same payload returns the cached * proposal id without re-running pre-prepare. * * • Fault detection. A "faulty" proposer is one that votes `no` in * the prepare phase (or doesn't vote at all within the timeout). * `viewChange(reason)` walks the round-robin schedule to the next * peer and increments `viewNumber`. */ import { PHASE_ORDER, type CommitResult, type ConsensusOpts, type ConsensusStatus, type Decision, type Phase, type ProposeResult, type Proposal, type ProposalSnapshot, type ProposalStatus, type Vote, type VoteResult, type ViewChangeResult } from "./types.js"; export interface ByzantineConsensusOpts extends ConsensusOpts { /** Wall-clock timeout (ms) after which a pending proposal expires. * Defaults to 5 seconds. */ timeoutMs?: number; /** Maximum number of proposals to retain in the history (LRU eviction). * Defaults to 100. */ historyLimit?: number; } /** * In-process Byzantine fault-tolerant consensus. One instance per * (cluster, quorum) tuple. Thread-unsafe — single-threaded JS so * the concern is theoretical, but document it for callers that wrap * the instance in worker threads. */ export declare class ByzantineConsensus { private readonly localAgentId; private readonly peers; private readonly quorum; private readonly maxFaults; private readonly timeoutMs; private readonly historyLimit; private readonly now; private readonly queen; private viewNumber; /** Live proposals, keyed by proposalId. */ private readonly proposals; /** Payload digest → proposalId, for replay protection. */ private readonly payloadIndex; /** Total counts (status === 'committed' / 'rejected' / 'expired'). */ private committed; private rejected; private expired; private viewChanges; constructor(opts: ByzantineConsensusOpts); /** Identity of the local agent (mirrors `opts.localAgentId`). */ getLocalAgentId(): string; /** Maximum Byzantine faults the cluster is sized for. */ getMaxFaults(): number; /** Maximum proposal history retained (LRU eviction). */ getHistoryLimit(): number; /** Current proposer (head of the round-robin schedule). */ getCurrentProposer(): string; getViewNumber(): number; getQuorum(): number; getPeers(): string[]; /** * Initiate pre-prepare for a new proposal. Generates a stable * proposalId from the payload + viewNumber. If the same payload is * re-submitted in the same view, the existing proposal is returned * (replay protection — see F-039 spec). */ propose(payload: unknown): ProposeResult; /** * Cast a vote for a peer during the `prepare` phase. Returns the * updated tally; if the proposal reaches quorum the phase auto- * advances to `commit` and the `reply` snapshot is recorded. * * If the proposer themselves votes `no`, the proposal is * immediately rejected (self-fault detection) — view-change is the * caller's responsibility (call `viewChange`). */ castVote(proposalId: string, agentId: string, decision: Decision, signature?: string): VoteResult; /** * External entry-point that lets the orchestrator deliver a peer * vote that arrived over a transport. The `proposalId` is passed * explicitly (it's not part of the `Vote` shape — Votes are scoped * to a single proposal inside the cluster). */ onPrepare(proposalId: string, vote: Vote): VoteResult; /** * External entry-point for a peer that has accepted the * pre-prepare. In the in-memory thin port this is mostly a * confirmation that the peer transitioned to `prepare` — we * record the pre-prepare accept and let `onPrepare` / `castVote` * carry the actual vote. */ onPrePrepare(peerId: string, proposalId: string): { proposalId: string; phase: Phase; }; /** * Mark a proposal as committed (used when the orchestrator wants * to force a commit, e.g. for tests that skip the vote tally). */ commit(proposalId: string): CommitResult; /** * Trigger a view-change. Advances the round-robin schedule to the * next eligible peer (lowest fault count), increments viewNumber, * and increments the viewChanges counter. The currently-active * proposal (if any) is marked with the given `reason` and frozen * at its current phase — the orchestrator can re-submit the same * payload under the new view. */ viewChange(reason: string, proposalId?: string): ViewChangeResult; /** Read-only snapshot of every proposal the instance has tracked. */ status(): ConsensusStatus; /** Snapshot of a single proposal, or `undefined` if unknown. */ getProposal(proposalId: string): ProposalSnapshot | undefined; private snapshotVoteResult; /** * Promote a proposal from `pre-prepare` to `prepare`. In the thin * port this is synchronous — we don't simulate network latency. */ private advanceToPrepare; /** * Returns true if a proposal in `prepare` can still reach quorum * given the remaining un-voted peers. Used to short-circuit * dead proposals into `rejected` early. */ private canStillReachQuorum; /** * Walk the proposal map, mark anything older than `timeoutMs` as * expired, and trim history to `historyLimit` entries. */ private evictExpired; /** * Stable hash of a payload for replay protection. The viewNumber is * included so the same payload in a new view produces a fresh digest * (view-changes are explicitly allowed to re-propose). */ private payloadDigest; } export type { CommitResult, ConsensusOpts, ConsensusStatus, Decision, Phase, ProposeResult, Proposal, ProposalSnapshot, ProposalStatus, Vote, VoteResult, ViewChangeResult, }; export { PHASE_ORDER }; //# sourceMappingURL=byzantine.d.ts.map