raft: trace validation of the shell against Raft.tla (tla-trace) - #27
Open
kiranandcode wants to merge 8 commits into
Open
kiranandcode wants to merge 8 commits into
kiranandcode wants to merge 8 commits into
Conversation
…dation
One JSON line per model step ({step, params, state}) after a header
naming the module, in the Verus exporter's value encoding, written to a
per-thread log opened by start() and closed by finish(); trace_step!
logs a step, and evaluates nothing when no log is open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each call of a verified step function in the shell (node.rs) logs the safety model transitions it performs with trace_step!: the t_* name, the model's parameters (ranks, terms, indices, and the ack map q a leader commits on), and the stepping node's observed host state after it (term, vote, role, log, commit; votes while a candidate, read_seq while a leader), read by a feature-gated view of Abs outside verus!. Where one step function performs several transitions (a follower heartbeat: ack, read confirmation, commit), each is logged, the earlier ones without the commit they precede. A restart (Node::new on a used log) is t_restart. With --features tla-trace, every node goldenscript writes its log to target/tla-traces/node/<script>.ndjson. Without the feature nothing changes; verified code is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cluster leader=N recorded the peers' votes directly with collect_vote, so no Vote message stood behind them: a step log of any such script leaves the safety model at t_become_leader (Raft.tla's t_collect_vote needs the Vote in the network). The harness now campaigns and stabilizes instead, as a real election would; every goldenscript's output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A one-node cluster elects itself as its node starts, so a log opened after the nodes were added began at a leader. The header now holds the model's initial hosts and the log sees the self-election. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t Raft.tla The hand-written counterpart of the exporter's trace spec, with the same interface (TraceLog, one VARIABLE, TraceInit, TraceNext, TraceEnabled, TraceDiagnosis), so tlc_conform runs either. TraceNext takes Raft.tla's action for the logged node, pins what it binds from the network with the other logged parameters (the Append, ack or read confirmation added, the ack map a commit rests on), and compares the observed host in the exporter's encoding (votes as ranks or Nil, roles as strings, commands as Nil or some element of Command). Each arm is a disjunct of Next, so Next is not conjoined again: that would make TLC enumerate t_leader_commit's ack maps at every step. tla/conform.sh runs the node goldenscripts with the tla-trace feature and checks every log with TLC: all 58 conform, each with exactly one explaining behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shell now logs the follower's match_index (mi) with t_recv_append, and Raft_trace.tla takes the step as RecvAppendAt: t_recv_append's body with the message's term, base and base term pinned to the logged ones and the ack to MAck(i, term, mi). Before, the arm pinned only the term, so TLC could explain the step by any Append in net and add the ack that one implied, which a later t_leader_commit's q could then rest on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Test job now runs clippy and the tests with --features tla-trace, and fmt, clippy and the tests of the tla-trace crate (not a workspace member). A new Trace validation job downloads the pinned BasisResearch/tlaplus jar (sha256-checked) and runs tla/conform.sh on the election script's log and two known-bad fixtures in tla/traces/, which must diverge at the step their header's "expect_divergence" names: a commit on a non-quorum ack map (step 15) and a follower acking past the Append it applied (step 10; the previous trace spec accepted it). conform.sh takes log paths as well as script names, runs the tests only when a script log is wanted (clearing stale logs first), and fails when TLC reports no search depth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eartbeat ack sent The Trace validation job ran conform.sh on election alone, leaving the other 57 scripts' conformance unchecked. It now runs conform.sh over every node goldenscript's log, then over tla/traces/*.ndjson. The follower heartbeat path logged t_send_ack with the heartbeat's last_index; it now logs plan.match_index, the value the HeartbeatResponse carries (equal when non-zero, by follower_heartbeat's postcondition). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the toyDB half of
plans/tlc_conform.md: trace validation of the Raft shell. With--features tla-trace, the shell logs every safety-model transition it performs. TLC then checks each log against the model, here the hand-writtentla/Raft.tlaoracle (the exported Raft module is being produced separately).Based on
yl/raft-safety-refine(#16), which carriestla/Raft.tla(#25).What was built
tla-trace/is a small, dependency-free crate. It writes the per-thread log: a header, then one JSON line per step, in the Verus exporter's value encoding (Value,start/finish).trace_step!logs a step and evaluates nothing when no log is open.tla-tracefeature, with no effect without it.node.rs) logs thet_*transitions that call performed, with the model's parameters (ranks, terms, indices), through a localtrace_step!.term,vote,role,log,commit, plusvoteswhile a candidate andread_seqwhile a leader. It is read by a feature-gated view ofAbsinrefine.rs, placed after theverus!block; verified code is untouched.qit rests on, which the leader knows from its match indexes. Otherwise TLC would branch over every ghostq.Node::newon a used log ist_restart.target/tla-traces/node/<script>.ndjsonwhen built with the feature. The header holds the model's initial hosts.tla/Raft_trace.tlais the oracle's trace spec. It has the same interface as the exporter's generated one (TraceLog, oneVARIABLE,TraceInit,TraceNext,TraceEnabled,TraceDiagnosis), so verus-tools-mcp'stlc_conformruns either.tla/Raft_trace.cfgconfigures it, andtla/conform.shruns every script's log through TLC.Result: all 58 node goldenscripts conform
Each script conforms with exactly one explaining behaviour (distinct states = steps + 1). The scripts range from 0 to 248 steps, on 1 to 7 nodes.
The first run found one divergence, in the test harness, not the shell.
cluster ... leader=Npromoted its leader by callingcollect_votefor every peer directly, with no Vote message behind the votes. 41 of the 58 scripts are built that way. The first run checked 47 scripts before my survey script crashed; 35 of those were built that way, and all 35 left the model at step 2,t_become_leader: Raft.tla'st_collect_voteneeds the Vote in the network, so the leader's vote set was{0}. The harness now elects through the message path (campaign, then stabilize). Every goldenscript's output is unchanged.A second issue was in my own harness code: a one-node cluster elects itself inside
Node::new, so its log must open before the nodes exist.Deviations from the plan
trace_step!is called at the step-function call sites in the shell, not inside the verified step functions.&Log, and reading the log's entries needs&mut.external_bodytaking&mut Logwould havoc the log in the proofs.Raft_trace.tladoes not conjoinNextagain, unlike the generated trace spec. EachTraceSteparm ist_x(i)for a nodei, which is a disjunct ofNext. Re-evaluating all ofNextmade TLC enumeratet_leader_commit's ack maps[Q -> 0..MaxLog]at every step, past a million functions at N = 5. For the same reason,t_leader_commitis taken at its logged witness(Q, q)(LeaderCommitAt, its body with the existentials instantiated).Command(A2), and the trace runs withCommand = {c1}.TOYDB_TLA_TRACE_MODULErenames the header's module and the same logs apply. One caveat: the generated trace spec needs every step parameter logged or finitely bounded, and the shell cannot log ghost payloads such asclog/vlog/rec.Test plan
cargo test(without the feature): every raft goldenscript passes, output unchanged by the harness election change.cargo test --features tla-trace --lib raft::node::testswrites 58 logs.TLA2TOOLS_JAR=<fork jar> tla/conform.sh: all 58 conform, exit 0.tlc_open tla/Raft.tlathentlc_conformonelection.ndjsonwithconstants: ["N = 3", "MaxTerm = 20", "MaxLog = 50", "MaxRead = 50", "Command = {c1}"]: conforms (20 steps, 21 states). A copy loggingci = 2for a one-entry leader diverges at step 15: the step is not enabled, and the model state and enabled steps are reported.cargo fmt --check;cargo clippy --tests --no-deps -- -D warnings, with and without--features tla-trace;cargo doc --no-deps;tla-tracecrate tests and clippy.t_recv_appendis pinned to the logged Append (term, base, base term) and to the ack built from the loggedmatch_index(RecvAppendAt). All 58 scripts still conform.--features tla-trace, plus fmt, clippy and the tests of thetla-tracecrate. A new Trace validation job downloads the pinnedbasis-11305b4a05jar (sha256-checked) and runstla/conform.shover every node goldenscript's log (all 58 must conform), thentla/conform.sh tla/traces/*.ndjsonover the two known-bad fixtures.election-bad-quorummust diverge at step 15 (a commit on a non-quorum ack map).election-bad-ackmust diverge at step 10 (a follower acking past the Append it applied); the unpinned spec accepted this one.cargo-veruson this machine). Theverus!block is unchanged; the newrefine.rscode is a feature-gated module after it.Companion PRs: BasisResearch/verus#54 (the exporter's trace spec) and BasisResearch/verus-tools-mcp#59 (
tlc_conform).🤖 Generated with Claude Code