Skip to content

Verified raft state machine - #16

Open
yiyunliu wants to merge 7 commits into
mainfrom
yl/raft-safety-refine
Open

Verified raft state machine#16
yiyunliu wants to merge 7 commits into
mainfrom
yl/raft-safety-refine

Conversation

@yiyunliu

Copy link
Copy Markdown

No description provided.

@yiyunliu

Copy link
Copy Markdown
Author

Reviewed the code with prompts from review.md. Got some feedback and currently running another session to fix the issues.

yiyunliu and others added 7 commits August 31, 2026 21:20
…mit, splice)

Extract the Raft log's in-memory state into LogState and route every state
change in Log through verified transitions: append_state (entry at
last_index + 1 in the current term), commit_state (commit index never
regresses), set_term_vote_state (nonzero, monotone term; vote can't change
within a term), check_splice_entries (contiguous indexes, monotone terms),
check_splice_connect (splice connects to the log without term regression),
and splice_state (never writes at or below the commit index). Each verdict
is proven exact, so Log panics with the same messages in the same cases;
transitions preserve the wf invariant (last_term <= term, commit_index <=
last_index), which is preserved rather than assumed since Log::new loads
unverified disk state. Storage, serialization, and the splice skip-scan
remain trusted. Opt raft::log into scripts/verus/verify.sh (42 -> 51
verified items).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UySbe8KkQbrWhQwiq1rtP1
…fety, log matching, leader completeness, state machine safety, linearizable reads)

Add src/raft/safety.rs: a spec/proof-only Verus model of the whole Raft
ensemble — per-host state, a monotone message history (drop/reorder/duplicate
allowed), and ghost per-term leader logs, election records, and commit
records — whose 14 transitions mirror the protocol steps of RawNode<Role> in
node.rs (each documents the code it models). Over all reachable states, prove:

- Election safety: at most one leader per term, from vote-once-per-term (the
  set_term_vote rule) plus quorum intersection.
- Log matching: entries pin their log's prefix to the creating term's leader
  log; two logs agreeing on an entry's term agree on the whole prefix.
- Leader completeness: a committed prefix is contained verbatim in every
  later leader's log. The inductive core: a commit's ack quorum intersects
  every later election's vote quorum; conditional persistence invariants
  (over current logs, and over log snapshots frozen in votes and election
  records) show the shared voter still held the prefix, and the section
  5.4.1 up-to-date check transfers it into the winner's log. The section
  5.4.2 own-term commit restriction of maybe_commit_and_apply is what makes
  the ack quorum meaningful.
- State machine safety: hosts never disagree on committed entries; splices
  can't truncate a committed prefix because the append source agrees with it
  by leader completeness (this justifies the splice-below-commit panic).
- Linearizable reads: a read served after quorum read-seq confirmation with
  an own-term committed tail (maybe_read's conditions) reflects every write
  committed anywhere at submission time, via read-confirm/ack quorum
  intersection and term monotonicity.

The proof is an inductive global invariant (init_implies_inv +
step_preserves_inv per transition, no admits/assumes), transported to all
executions by execution_implies_inv; thm_* state the headline properties.
This is a model-only proof in the TLA+/IronFleet-protocol-layer sense: the
correspondence to node.rs is by documented inspection, not machine-checked
refinement (that ironkv-style layer is future work), and liveness is out of
scope. Everything erases under a normal cargo build. Opt raft::safety into
scripts/verus/verify.sh (51 -> 90 verified items).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UySbe8KkQbrWhQwiq1rtP1
…nsitions

Add src/raft/refine.rs, the node-local refinement layer connecting node.rs to
the raft::safety model, and restructure node.rs to route every
safety-relevant decision and state change through its verified step cores.

Each core (core_grant, core_campaign, core_collect_vote, core_become_leader,
core_propose, core_recv_append, core_leader_commit, core_recv_commit,
core_send_append/ack/commit, core_submit_read, core_confirm_read,
core_bump_term, core_step_down, core_restart) takes the node's concrete state
summary plus its ghost abstract MHost state and proves host_refines: in every
cluster state consistent with the node's local view and message evidence,
performing this step is a transition of the safety model. Per-transition
lifting lemmas construct the global witness state. The protocol decisions are
computed by the cores and executed mechanically by the shell: the vote-once +
section 5.4.1 up-to-date check (core_grant), the strict-majority quorum
arithmetic for elections and commits (verified counting loops that also build
the ghost ack-quorum evidence maps), the section 5.4.2 own-term commit
condition, the append base-match/splice/ack computation, and the
linearizable-read gate (core_can_serve), whose postcondition instantiates
thm_read_linearizable: a served read reflects every write committed anywhere
at submission time.

Model additions to support write-level granularity: t_bump_term (a
higher-term discovery as its own transition, also covering a crash between
the term bump and the follow-up step) and t_step_down (candidate to follower
in the same term); commands become opaque byte sequences.

The trusted rim is explicit and greppable (refine.rs module docs): network
non-forgery (received messages are in the ghost history; recv_* axioms
recover ghost payloads behind concrete summaries), storage integrity (Log
answers agree with the ghost log view), the mechanical shell discipline in
node.rs (core preconditions at call sites are not machine-checked), and
agreed cluster membership (model hosts are member ranks). node.rs carries the
ghost state as zero-sized Ghost fields (habs, evid); a normal build erases
all of it. The test harness's direct leader promotion now records the peers'
votes so the verified quorum check is satisfied.

Opt raft::refine into scripts/verus/verify.sh (90 -> 131 verified items).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UySbe8KkQbrWhQwiq1rtP1
Review follow-ups for the Raft safety/refinement layer:

- Model: t_restart now takes the recovered commit index (c <= commit),
  since Log::commit does not fsync it; commit_leader_ok only claims a
  covering commit index while the host is still leader of that term, so
  thm_read_linearizable is unchanged. recover_abs documents exactly which
  model step it stands in for and the remaining fsync=false gap.
- Cores check what the shell previously had to promise: member ids are
  verified distinct and in range at runtime (check_members) in
  core_leader_commit/core_can_serve; core_campaign and core_recv_append
  return None on term/index overflow instead of requiring it away.
- Send-only cores return a #[must_use] Refined token consumed by
  send_refined/broadcast_refined (note_own_ack for the leader's self-ack),
  so a dropped core call is a compiler warning rather than a silent gap.
- into_leader and propose share append_and_replicate; dead core_restart,
  lemma_lift_restart and cmd_view removed; mod refine no longer allows
  dead_code; document that the receiving cores only cover the equal-term
  path (the shell bumps first via core_bump_term).

Verus: 134 verified, 0 errors. All tests pass; 300 random goldenscripts
produce identical output against main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuukBNSENwNnQvaHZSXwhG
… to end

Address the review findings on the refinement layer: the safety theorems
were connected to the running code only through ~40 unchecked `requires`
clauses on flat step cores, discharged by nobody in the unverified shell.
Move those obligations into Verus-checked code:

- Log (raft::log) now lives inside verus! with a ghost view of the stored
  entries. Every method pins its result and the new view (splice provably
  computes the model's splice, skip-scan included; has/get/read_range
  answer per the view). The trusted rim shrinks to the engine I/O
  functions plus Log::open, each with an explicit storage-integrity spec.
  Panics are preserved verbatim via a single diverging `fault` function.

- A verified Members type (sorted ids, rank = position) replaces the
  shell's ad-hoc id->rank conversion, fixing the recover-time vote
  rank/id confusion and the 256-node size wrap.

- Candidate votes and leader Progress move into the verified Abs state;
  Abs::inv backs every nonzero match index / read sequence with ack or
  confirmation evidence in the ghost history, so the commit quorum and
  the linearizable-read gate derive their evidence instead of trusting
  the shell to have recorded it.

- One verified step function per protocol input composes the model
  transitions internally via host_refines_star (reflexive-transitive
  refinement that also carries the model invariant through
  step_preserves_inv). node.rs shrinks to an I/O shell with no core
  calls and no ghost values; the Refined token and note_* helpers are
  gone.

- The four recv_* network axioms collapse into a single trusted
  recv_msg whose spec pins every concrete field of the received
  message, Append entry terms and commands included.

scripts/verus/verify.sh: 177 verified, 0 errors (from 134). cargo test,
clippy -D warnings, and fmt all green; raft goldenscripts unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYvjWPjYtMgmPqUE3d5SWK
…osition

Address the three follow-ups from the round-2 review:

- M2 (apply path): add a verified `Log::read_committed` over the trusted
  engine rim, whose postcondition pins the returned batch to the committed
  prefix of the log's ghost view, and route `maybe_apply` /
  `maybe_commit_and_apply` through it. The state machine is now fed exactly
  the committed prefix the safety theorems are about; the unverified
  `scan`/`scan_apply` iterators become test-only. Temper the docs that said
  "applied prefix" where the theorem covers the committed prefix.

- m1 (mutator visibility): nest `raft::log` and `raft::refine` under a
  shared `raft::verified` parent module (files stay in place via #[path])
  and scope `Log::{set_term_vote, append, commit, splice}` to
  `pub(in crate::raft::verified)`. The unverified I/O shell now gets read
  access only, so a future node.rs edit cannot mutate the log behind the
  refinement layer's back and silently vacate `Abs::inv`.

- M1 (composition): state trusted assumption 3 formally instead of in
  prose. `cluster_bound` defines the composition invariant (a reachable,
  invariant-satisfying model state binding every node's ghost state and
  evidence); machine-checked lemmas establish it for a fresh cluster
  (`lemma_cluster_init`) and maintain it across every step function call
  (`lemma_cluster_step`) and crash-restart (`lemma_cluster_restart`);
  `thm_cluster_safety` / `thm_impl_safety` instantiate the model's safety
  theorems against a bound cluster — the latter directly against the
  verified node states and the logs' verified views. The trusted residue is
  reduced to the lemmas' hypotheses tracking the real run (assumptions 1,
  2, 5).

The module rename perturbed two seed-sensitive SMT queries in log.rs
(`splice`, `count_present`); they get rlimit headroom and (for the latter) a
spinoff prover. Verification: 184 verified, 0 errors. Tests: 286 lib + 5
integration pass; goldenscripts unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYvjWPjYtMgmPqUE3d5SWK
…coped Log mutators

main added its own [lints.rust] table alongside the one this branch added,
so cargo rejected Cargo.toml with 'duplicate key' on the PR merge commit.
Keep main's copy only.

Log::append/splice/commit are now pub(in crate::raft::verified), so the
intra-doc links in Log's docs pointed at private items and failed
cargo doc under -D warnings. Use plain code spans instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYvjWPjYtMgmPqUE3d5SWK
@yiyunliu
yiyunliu force-pushed the yl/raft-safety-refine branch from 6a4fc83 to 3d773dd Compare September 1, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant