Repository navigation
Lean spec of ccfraft and trace validation - #8382
cjen1-msft wants to merge 49 commits into
Conversation
Add a standalone executable Raft model and user-visible safety predicates. Reduce raw raft_driver captures into deterministic actions and observations with source provenance and documented callback abstraction boundaries. Align bootstrap, batching, packet loss, response handling, and retirement with the implementation. Cover every raft scenario, add negative replay regressions, and include Lean tooling in repository Python checks and CI. Keep the safety-proof port separate from this trace-validation checkpoint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the existing inductive safety argument without changing the executable model. Keep the system invariant, ghost histories, and supporting lemmas under Proofs, with reviewed predicates in Protocol and public statements in Properties. Prove reachable election safety, committed-prefix agreement, and signature commit frontiers. Adapt the argument for physical bootstrap, packet loss, batched replication, inactive owners, and updated handler metadata. Exclude independent retirement guarantees and liveness claims. Require complete library imports and the pinned axiom audit used by the disaster recovery package. Keep replay executables independent of proofs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend tla-shallow.yml to install the pinned Lean toolchain, build the ccfraft-replay executable, and replay every captured Raft scenario after the existing raft_driver build, uploading trace/diagnostic artifacts alongside the TLC output. Document the combined pipeline in the Lean project README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
A few initial thoughts:
|
|
Heidi Howard (@heidihoward) sorry for the essay...
Definitely, lots can be shared.
Yep thats agreed. I was looking into it as I expect we will not want to drastically change the model after we hit merge. And ensuring we are not wholly locked out of production tracing was important to work out.
You are correct, we can just align the model with the implementation, I was aiming for something comparable with ccfraft.tla for this model but a more aligned one is also possible. I'm not sure this is exactly what we want to do as proof repair becomes an expensive concern, say if we add another trace event to raft.h or reorder when things get sent. We have two somewhat orthogonal aims here
The discovery requires that we either use some search function to discover this, which becomes intractable quickly (infinite models etc), or SMT scaling problems. The reduction approach is just a way to represent that mapping. I don't fully see the tradeoff between python and lean as the host for the mapping. |
|
Also to capture a thought. This can be modelled either as N -> 1 reduction. The issue is that the 1->1 model must reconstruct the serial execution of the raft.h, via sequencing or phases. |
Split the global-state model into a node-local step (Model/Node.lean, Model/Local.lean) composed by the shared MultiNodeTransitionSystem, copied from the disaster-recovery package. A delivery now adopts a newer message term and handles the message in one step, replacing UpdateTerm. Messages lose their source and destination fields, which the envelope carries. Global join and transaction-identifier histories are gone. The replayer resolves each receive to the oldest undropped envelope and records drops without stepping the model. The reduction emits one receive per packet. The old global model and its invariant proofs move to Proofs/Abstract, sharing the node-level definitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Proofs/Refinement relates each reachable state of the network model to an abstract global-state model that satisfies the existing inductive invariant. A concrete step is simulated by reordering one abstract queue, an abstract term update when the receiver adopts a newer term, an abstract same-term step-down, and the abstract action with the same name. The abstract model drops its join-once and unique-transaction-identifier guards, which the invariant proofs never used. Properties.lean states election safety, committed-log prefix agreement, signature commit frontiers, and append-only committed logs over valid traces. Proof.lean links each claim, and Witnesses.lean gives a concrete kernel-checked trace satisfying the premises of each. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Describe the node-local model, the network composition, the properties, and the refinement proof in the package README. Bump the replay schema to ccfraft-replay/v3, because a receive now adopts a newer term itself and v2 documents contain separate updateTerm actions. Format the model with the pinned leanfmt, and build the whole package with --wfail in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply the pinned leanfmt at its default line width to every tracked Lean file in lean/ccf-raft, matching the repository Lean formatting check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tion # Conflicts: # .github/skills/formatting-and-linting/SKILL.md
leanfmt loads each file's imports, so the CCF Raft files need that package's environment. Check each package from its own directory, keep files outside lean/ with the disaster recovery package, and run the check for its own package in each Lean CI job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add NodeInvariant, which lifts a predicate that every local step preserves to every node of every reachable state, and use it to prove CommittedFrontierIsSignature without the abstract model. Move the concrete-model facts the refinement shared into Refinement/Concrete.lean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A local step never lowers the commit index and never rewrites the log at or below it, given the commit frontier invariant. A network step changes only the acting node, so the property needs no global reasoning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the same-state CommittedLogsPrefix and the per-step CommittedLogAppendOnly claims with one property: any two committed logs in a valid trace, from any nodes in any states, are prefix-comparable. The proof carries the earlier node's committed log forward, where it has only grown, and compares the two logs in the later state. The append-only step lemma and same-state agreement remain as proof lemmas. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…model Proofs/Abstract and Proofs/Refinement are removed. Proofs/Invariant/View.lean computes the global view of a network state that the invariant reads, and Proofs/Invariant/Facts.lean restates the old invariant definitions over that view. Proof.lean and Proofs/CommittedLogs.lean do not build yet: the property derivations and the step preservation proofs are still to port. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed invariant Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…View Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ervation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mit advancement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…votes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed endpoints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove reachable_inv by induction over concrete local and delivery steps. Term observation, same-term step-down, message erasure, and replies preserve the invariant without a second transition system. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove 71 unused migrated lemmas and the unused message-source guard. Add a kernel dependency report for future proof cleanup, and document the concrete-step invariant argument. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use endpoint/payload tuples for immutable message histories and quantify the joined set separately. Facts builds independently; preservation consumers are migrated in the following units. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove copied request handlers and metadata-erasure adapters from proof operations. Prove local handler facts against Model.Local and frame configuration coverage through concrete node lookup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use concrete node-list replacements, envelope sends, and joined-set side conditions. The replacement StateFacts and Effects modules compile independently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep joined-set growth separate from node-list updates. Characterize immediate acknowledgement through the actual follower acceptance handler, without clearing retirement metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Characterize ACK eligibility using the concrete acceptance handler. Replace queue updates with removal from the envelope list and preserve transfer to processed match indices. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The full package builds with warnings as errors, and the axiom audit permits only the existing standard axioms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Retain the node-lookup network simp rule used during elaboration. Remove seventeen declarations outside both exported proof dependencies and explicit tactic references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Carry node-table distinctness in StateInvariant and use it directly in safety projections. Keep the joined set as existential ghost state and describe endpoint/payload history keys. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consume the RDP_TRACE format of #8366 with the reduction and replay design of #8382, replacing the separate disaster-recovery-trace validator package. - replay/reduction.py reduces the node logs of one e2e scenario to disaster-recovery-replay/v1 actions and observations. Records are ordered by each node's sequence, receives are linked to the send their caused_by names, and each retry batch is one model retry. Handler records are speculative, so the reduction infers which executions took effect from later records and committed records: superseded re-executions and stale reads did not, a vote read while Gossiping may have committed before an earlier transition, and the newest phase writes may stay unconfirmed. Every instruction names its rule and log record. Trace failures, receives without caused_by, and records that no account explains fail the run. - DisasterRecovery/Replay.lean replays the document through Model.transitionSystem. Actions must be enabled, and observations must match the model's node state or the messages and notifications of the node's latest action. replay/ReplayMain.lean is the disaster-recovery-replay executable. - replay/validate_logs.py waits for complete logs, then reduces and replays them, keeping the artifacts. tests/infra/recovery_trace.py only passes it the log paths and scenario expectations. - Python tests in replay/tests cover the rules, including re-executions sharing caused_by, unconfirmed final executions, receives before the first committed record, repeated restarts in Joining, and rejection of missing caused_by and trace failures. The Lean workflow runs them. The Genoa SNP job builds the replayer and uploads each scenario's replay artifacts. - As in #8382, the Python format and lint checks now cover lean/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ruff EXE001 requires files with a shebang to be executable, as #8382's are. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consume the RDP_TRACE format of #8366 with the reduction and replay design of #8382, replacing the separate disaster-recovery-trace validator package. - replay/reduction.py reduces the node logs of one e2e scenario to disaster-recovery-replay/v1 actions and observations. Records are ordered by each node's sequence, receives are linked to the send their caused_by names, and each retry batch is one model retry. Handler records are speculative, so the reduction infers which executions took effect from later records and committed records: superseded re-executions and stale reads did not, a vote read while Gossiping may have committed before an earlier transition, and the newest phase writes may stay unconfirmed. Every instruction names its rule and log record. Trace failures, receives without caused_by, and records that no account explains fail the run. - DisasterRecovery/Replay.lean replays the document through Model.transitionSystem. Actions must be enabled, and observations must match the model's node state or the messages and notifications of the node's latest action. replay/ReplayMain.lean is the disaster-recovery-replay executable. - replay/validate_logs.py waits for complete logs, then reduces and replays them, keeping the artifacts. tests/infra/recovery_trace.py only passes it the log paths and scenario expectations. - Python tests in replay/tests cover the rules, including re-executions sharing caused_by, unconfirmed final executions, receives before the first committed record, repeated restarts in Joining, and rejection of missing caused_by and trace failures. The Lean workflow runs them. The Genoa SNP job builds the replayer and uploads each scenario's replay artifacts. - As in #8382, the Python format and lint checks now cover lean/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ruff EXE001 requires files with a shebang to be executable, as #8382's are. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This is a port of ccfraft.tla over to lean.
I've tried to keep the structure similar to the DR work, and am very willing to align this further, and also change up the model substantially if required. (proof work will need to be repaired unfortunately)
This has
Reduction
There is some additional work here that was unnecessary on the other purpose-built protocols.
I tried to align this model with ccfraft.tla (possibly unnecessarily), which means that we have have to deal with non-determinism in the mapping between the model and the code.
For this I'm proposing that we extend the python side to have what I'm calling a reduction step.
As we control both the code and the model, we can use this to deterministically align the 'grains of atomicity' between the two.
This is what that looks like, we have rules in python which reduce a matching set of instructions to a set of actions.
e.g. reduction.py sees node 1 (stale candidate, term 2) get an AppendEntries at term 4. Because the paired become_follower shows a strictly higher term, it splits the one C++ callback chain into two deterministic canonical actions plus observations:
[ {"observation": "state", "node": "1", "fields": {"role": "candidate", "currentTerm": 1}}, {"observation": "message", "source": "0", "destination": "1", "packet": {"msg": "raft_append_entries", "term": 3}}, {"action": "updateTerm", "source": "0", "destination": "1"}, {"observation": "state", "node": "1", "fields": {"role": "follower", "currentTerm": 3}}, {"action": "receive", "source": "0", "destination": "1"}, {"observation": "state", "node": "1", "fields": {"role": "follower", "currentTerm": 3}}, {"observation": "message", "source": "1", "destination": "0", "selection": "last", "packet": {"msg": "raft_append_entries_response", "term": 3}} ]The pipeline I'm proposing for this is:
The property we get out of trace validation is the alignment between the code and the model.
This alignment is dependent on how well we constrain the model to match the code.
This approach I think should allow us to maximise that alignment and make each step separately auditable.
Comments
SMT trace validation
This trace validation relies on a concrete initial state that we then trace from.
This is not an option for production, as we need to be able to create an initial state from the remote.
We can hack around this by trying to infer what that initial state should be, or producing something 'good-enough', but we will be stuck with a violation trying to work out whether the initial inference was broken or the actual implementation.
Fundamentally what we get from a trace is a set of constraints in the form of observations about each state, and the actions that each node should have taken.
This constitutes a formula that we can then 'just check', and for small traces this works.
The problem is the representation of the trace.
For example supposing that your ledger representation became: fn candidate : predicate(c)
Then to simply evaluate the final state of the trace you have to go via 50-500 of those lambdas.
And tbh SMT trace validation seems to be a mine-field of exactly these kind of issues.
So for example:
Additionally as soon as you do this, your trace validator has drifted from your original model.
In theory, and in practise, you can then prove that the trace validator representation preserves the original properties, and largely this is a mechanical proof, but data-dependent paths in the model make this proof very expensive/difficult.
All of this is to say that this approach works, and can be made to work, but the cost is very high, and this might be a punt for the next generation of models, or improvements in the integration with SMT solvers.
Additionally small prototypes can scale absolutely fine, and then the full thing breaks horribly.
tl;dr
SMT solvers are likely the right approach for this, and the UX works for it. But getting it to scale as it should be possible (this is deterministic, and just filling in details) there are too many hard edges to go down this route yet.
Merging this PR
I'll also split this up further in the future to get it merged. This draft PR is mainly for discussion.