Skip to content

Commit 8eb6fe6

Browse files
committed
Cover remaining verification scenarios
1 parent 1a349d0 commit 8eb6fe6

5 files changed

Lines changed: 615 additions & 33 deletions

File tree

MODEL_EQ_REPORT.MD

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ This report maps the current TLA+ specification to the current Go implementation
2323
| Not represented in current TLA safety model | `epaxos/codec.go:40` Go decoder | Wire-decoder input tolerance is implementation validation rather than modeled state. |
2424
| Not represented in current TLA safety model | `epaxos/storage.go:9` Go storage interface | Storage virtualization is an implementation boundary used by deterministic tests rather than modeled state. |
2525
| Not represented in current TLA safety model | `epaxos/quorum.go:15` Go quorum calculation | Quorum arithmetic for cluster sizes one through seven is verified by Go tests; the current TLA constants enumerate voters directly. |
26+
| Not represented in current TLA safety model | `examples/kv/kv.go:102` Go transaction batch apply | Example KV transactions are opaque EPaxos commands at the model boundary; atomic multi-key apply is verified by Go tests rather than by the current TLA state. |
27+
| Not represented in current TLA safety model | `examples/kv/kv.go:169` Go scan implementation | Advanced scan ordering, reverse ordering, timestamp collision behavior, and Pebble durability are example storage semantics verified by Go tests rather than by the current TLA state. |
2628

2729
## Current verification evidence
2830

@@ -31,12 +33,14 @@ This report maps the current TLA+ specification to the current Go implementation
3133
- Observed `go test ./...` from `examples/kv` passing.
3234
- Observed `go test -coverprofile=coverage.out ./...` from `examples/kv` plus `go tool cover -func=coverage.out` reporting 100.0% statement coverage for the example module.
3335
- Observed `go test -tags kvnode ./cmd/kvnode` from `examples/kv` passing for the tagged HTTP service package.
36+
- Observed `go test ./...` coverage including cluster destruction and restart via `TestRestartAllRawNodesRetainsExecutedAndAppliesOnlyNewCommand`, equal-sequence execution ordering via `TestExecutionEqualSeqTieBreaksByRef`, and inactive dependency filtering via `TestExecutionComponentsSkipInactiveDependencyRefs`.
37+
- Observed `go test ./...` coverage in `examples/kv` including transaction atomicity, malformed transaction rejection, transaction batch error propagation, timestamp collision overwrite behavior, Pebble close/open durability, and reverse scans returning the newest version for repeated keys.
3438
- Observed TLC passing with `/opt/homebrew/opt/openjdk/bin/java -cp /tmp/tla2tools.jar tlc2.TLC -config tla/EPaxos.cfg tla/EPaxos.tla`, generating 11162 states, 2002 distinct states, and no invariant violation.
3539
- Observed `lein test` from `jepsen` passing namespace and dependency loading, and observed `tests/ci.sh` running a local three-node Jepsen workload against deployed `kvnode` processes with `lein run test --no-ssh --nodes 127.0.0.1:19081,127.0.0.1:19082,127.0.0.1:19083 --time-limit 5 --concurrency 3` reporting `:valid? true` after successful writes and reads.
3640
- Observed repository text audit with no disallowed external-project name, scaffolding marker, or wall-clock API matches.
3741

3842
## Open equivalence risks
3943

4044
- The TLA+ model is finite-bounded for TLC, so it demonstrates correspondence for the configured state space rather than unbounded proof.
41-
- Checksum, wire-decoder tolerance, storage virtualization, quorum arithmetic, and recovery response collection are verified by implementation tests but outside the current TLA safety state.
42-
- The local Jepsen run uses in-process loopback deployment without SSH-managed OS faults; external deployment validation can exercise a broader environment.
45+
- Checksum, wire-decoder tolerance, storage virtualization, quorum arithmetic, recovery response collection, and example KV storage semantics are verified by implementation tests but outside the current TLA safety state.
46+
- The local Jepsen run uses in-process loopback deployment without SSH-managed OS faults, and its workload currently covers register-style successful writes and reads rather than application-level transaction internals.

epaxos/sim_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,70 @@ func TestRestartFromMemoryStorage(t *testing.T) {
177177
}
178178
}
179179

180+
func TestRestartAllRawNodesRetainsExecutedAndAppliesOnlyNewCommand(t *testing.T) {
181+
ids := makeIDs(3)
182+
s := newSimCluster(t, len(ids), false)
183+
first := Command{ID: CommandID{Client: 1, Sequence: 1}, Payload: []byte("first"), ConflictKeys: [][]byte{[]byte("shared")}}
184+
second := Command{ID: CommandID{Client: 1, Sequence: 2}, Payload: []byte("second"), ConflictKeys: [][]byte{[]byte("shared")}}
185+
if _, err := s.nodes[1].Propose(first); err != nil {
186+
t.Fatal(err)
187+
}
188+
s.drain()
189+
if got := len(s.apps[1]); got != 1 {
190+
t.Fatalf("node 1 applied %d commands before restart", got)
191+
}
192+
firstRef := s.apps[1][0].Ref
193+
for id := range s.nodes {
194+
if got := len(s.apps[id]); got != 1 {
195+
t.Fatalf("node %d applied %d commands before restart", id, got)
196+
}
197+
if s.apps[id][0].Ref != firstRef {
198+
t.Fatalf("node %d first ref = %s, want %s", id, s.apps[id][0].Ref, firstRef)
199+
}
200+
}
201+
202+
s.nodes = make(map[ReplicaID]*RawNode, len(ids))
203+
for _, id := range ids {
204+
rn, err := NewRawNode(Config{ID: id, Voters: ids, Storage: s.stores[id], RetryTicks: 2, RecoveryTicks: 5})
205+
if err != nil {
206+
t.Fatalf("restart node %d: %v", id, err)
207+
}
208+
s.nodes[id] = rn
209+
}
210+
s.apps = make(map[ReplicaID][]CommittedCommand, len(ids))
211+
212+
if _, err := s.nodes[2].Propose(second); err != nil {
213+
t.Fatal(err)
214+
}
215+
s.drain()
216+
for id, rn := range s.nodes {
217+
applied := s.apps[id]
218+
if len(applied) != 1 {
219+
t.Fatalf("node %d applied %d commands after restart: %#v", id, len(applied), applied)
220+
}
221+
gotCmd := applied[0].Command
222+
if gotCmd.ID != second.ID ||
223+
!bytes.Equal(gotCmd.Payload, second.Payload) ||
224+
len(gotCmd.ConflictKeys) != 1 ||
225+
!bytes.Equal(gotCmd.ConflictKeys[0], second.ConflictKeys[0]) {
226+
t.Fatalf("node %d applied command = %#v, want %#v", id, gotCmd, second)
227+
}
228+
if applied[0].Ref == firstRef {
229+
t.Fatalf("node %d re-applied first ref %s", id, firstRef)
230+
}
231+
var hasFirst bool
232+
for _, ref := range rn.Status().Executed {
233+
if ref == firstRef {
234+
hasFirst = true
235+
break
236+
}
237+
}
238+
if !hasFirst {
239+
t.Fatalf("node %d executed refs lost first ref %s: %#v", id, firstRef, rn.Status().Executed)
240+
}
241+
}
242+
}
243+
180244
func TestLogicalTicksRecoveryAndStorageFailure(t *testing.T) {
181245
s := newSimCluster(t, 3, true)
182246
s.drop[[2]ReplicaID{1, 2}] = true
@@ -239,6 +303,68 @@ func TestQuorumTables(t *testing.T) {
239303
}
240304
}
241305

306+
func TestExecutionEqualSeqTieBreaksByRef(t *testing.T) {
307+
rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
308+
if err != nil {
309+
t.Fatal(err)
310+
}
311+
a := InstanceRef{Replica: 3, Instance: 1, Conf: 1}
312+
b := InstanceRef{Replica: 1, Instance: 1, Conf: 1}
313+
c := InstanceRef{Replica: 2, Instance: 1, Conf: 1}
314+
rn.instances[a] = &instance{rec: InstanceRecord{Ref: a, Status: StatusCommitted, Seq: 7, Deps: []InstanceNum{1, 0, 0}, Command: Command{Payload: []byte("a")}}}
315+
rn.instances[b] = &instance{rec: InstanceRecord{Ref: b, Status: StatusCommitted, Seq: 7, Deps: []InstanceNum{0, 1, 0}, Command: Command{Payload: []byte("b")}}}
316+
rn.instances[c] = &instance{rec: InstanceRecord{Ref: c, Status: StatusCommitted, Seq: 7, Deps: []InstanceNum{0, 0, 1}, Command: Command{Payload: []byte("c")}}}
317+
318+
comps := rn.executionComponents()
319+
if len(comps) != 1 || len(comps[0]) != 3 {
320+
t.Fatalf("equal-seq cycle components = %#v", comps)
321+
}
322+
if refs := append([]InstanceRef(nil), comps[0]...); len(refs) == 3 && refs[0] == b && refs[1] == c && refs[2] == a {
323+
t.Fatalf("test setup no longer exercises execution sort: %#v", refs)
324+
}
325+
326+
rn.tryExecute()
327+
rd := rn.Ready()
328+
want := []InstanceRef{b, c, a}
329+
if got := refs(rd.Committed); fmt.Sprint(got) != fmt.Sprint(want) {
330+
t.Fatalf("equal-seq execution order = %v, want %v", got, want)
331+
}
332+
for i, cmd := range rd.Committed {
333+
if cmd.Seq != 7 {
334+
t.Fatalf("committed[%d] seq = %d, want 7", i, cmd.Seq)
335+
}
336+
}
337+
}
338+
339+
func TestExecutionComponentsSkipInactiveDependencyRefs(t *testing.T) {
340+
tests := []struct {
341+
name string
342+
dep *instance
343+
}{
344+
{name: "missing"},
345+
{name: "not-yet-chosen", dep: &instance{rec: InstanceRecord{Ref: InstanceRef{Replica: 2, Instance: 1, Conf: 1}, Status: StatusAccepted, Seq: 1}}},
346+
}
347+
for _, tt := range tests {
348+
t.Run(tt.name, func(t *testing.T) {
349+
rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
350+
if err != nil {
351+
t.Fatal(err)
352+
}
353+
x := InstanceRef{Replica: 1, Instance: 1, Conf: 1}
354+
y := InstanceRef{Replica: 2, Instance: 1, Conf: 1}
355+
rn.instances[x] = &instance{rec: InstanceRecord{Ref: x, Status: StatusCommitted, Seq: 2, Deps: []InstanceNum{0, 1, 0}, Command: Command{Payload: []byte("x")}}}
356+
if tt.dep != nil {
357+
rn.instances[y] = tt.dep
358+
}
359+
360+
comps := rn.executionComponents()
361+
if len(comps) != 1 || len(comps[0]) != 1 || comps[0][0] != x {
362+
t.Fatalf("component with inactive dependency = %#v, want only %s", comps, x)
363+
}
364+
})
365+
}
366+
}
367+
242368
func refs(cmds []CommittedCommand) []InstanceRef {
243369
out := make([]InstanceRef, len(cmds))
244370
for i := range cmds {

0 commit comments

Comments
 (0)