Lean spec of ccfraft and trace validation - #8382
cjen1-msft wants to merge 9 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. |
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.