Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b918872
Add Lean disaster recovery transition model
achamayou Sep 4, 2026
497efc9
Prove local recovery safety and liveness
achamayou Sep 4, 2026
f242dd4
Add global recovery semantics and invariants
achamayou Sep 4, 2026
065dea3
Prove quorum and committed-prefix safety
achamayou Sep 4, 2026
47343b0
Prove fair global recovery progress
achamayou Sep 4, 2026
8a330b9
Add canonical Lean checks and CI
achamayou Sep 4, 2026
4c22830
Format Lean disaster recovery documentation
achamayou Sep 4, 2026
c3447aa
Normalize Lean source line endings
achamayou Sep 4, 2026
7ed0e2f
Separate Lean review contracts from proof implementations
achamayou Sep 7, 2026
5c8daa7
Consolidate Lean verification workflow
achamayou Sep 7, 2026
059dc03
Share Lean build ignore rule
achamayou Sep 7, 2026
b2f3932
Upgrade Lean disaster recovery to 4.33.1
achamayou Sep 7, 2026
0e89572
Align disaster recovery Lean namespaces with module paths
achamayou Sep 8, 2026
8cc52f8
Limit disaster recovery proofs to safety
achamayou Sep 9, 2026
971644a
Add temporary legacy Lean migration package
achamayou Sep 4, 2026
5e6d9c8
Add Rust exporter and legacy graph comparator
achamayou Sep 4, 2026
66e7932
Prove temporary canonical phase refinement
achamayou Sep 4, 2026
5ebeda9
Add temporary migration CI and documentation
achamayou Sep 4, 2026
bcf7071
Tighten temporary migration checks
achamayou Sep 4, 2026
02c1548
Enforce disaster recovery export invariants
achamayou Sep 4, 2026
252377d
Update migration evidence for Lean 4.33
achamayou Sep 7, 2026
0e75abe
Fix migration refinement namespace
achamayou Sep 8, 2026
f5ab02b
Remove superseded disaster recovery models
achamayou Sep 4, 2026
f491105
Keep scheduled Lean checks in shared workflow
achamayou Sep 7, 2026
07681f4
Defer recovery restart until commit
achamayou Sep 4, 2026
c364438
Add commit-aware recovery tracing
achamayou Sep 4, 2026
37fd251
Add isolated Lean trace validator
achamayou Sep 4, 2026
adc31fa
Add deterministic recovery trace extraction
achamayou Sep 4, 2026
bd2fa7e
Validate SNP recovery scenarios with Lean
achamayou Sep 4, 2026
5f8c298
Run trace ordering checks in CI
achamayou Sep 4, 2026
55751d2
Upgrade Lean trace validation to 4.33.1
achamayou Sep 7, 2026
bca9e9f
Fix trace validator model namespace
achamayou Sep 9, 2026
09f17b2
Merge main into PR #8282 and resolve Lean workflow conflicts
achamayou Sep 11, 2026
eb47e62
Merge latest main into PR #8282
achamayou Sep 11, 2026
9f959e3
Merge recovery implementation prelude into trace validation
achamayou Sep 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ and the proof implementation files marked as generated for review purposes.
The standard `mk_all --check` command ensures that the audit root imports every
library module, so newly added proofs cannot silently escape the checks.

The trace-validator job builds and audits the isolated strict trace validator,
then runs its parser, replay, extraction, and ordering checks. The Milan and
Genoa SNP jobs in `ci.yml` validate real committed C++ recovery traces and
upload the generated NDJSON evidence.

File: `lean.yml`
3rd party dependencies: None

Expand Down
38 changes: 36 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,28 @@ jobs:
python3 tests/infra/platform_detection.py snp milan
shell: bash

- name: "Build Lean recovery trace validator"
run: |
set -euo pipefail
curl --proto '=https' --tlsv1.2 -sSf \
https://raw.githubusercontent.com/leanprover/elan/58e8d545e33641f66dbcbd22c4283109e71757be/elan-init.sh \
-o /tmp/elan-init.sh
sh /tmp/elan-init.sh -y --default-toolchain none
rm /tmp/elan-init.sh
export PATH="${HOME}/.elan/bin:${PATH}"
elan toolchain install "$(cat lean/disaster-recovery-trace/lean-toolchain)"
cd lean/disaster-recovery-trace
lake exe cache get
lake build trace-validator
shell: bash

- name: "Build Debug"
run: |
set -ex
git config --global --add safe.directory /__w/CCF/CCF
mkdir build
cd build
cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DWORKER_THREADS=1 ..
cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DWORKER_THREADS=1 -DCCF_RECOVERY_TRACE=ON ..
ninja
shell: bash

Expand All @@ -314,6 +329,7 @@ jobs:
shell: bash
env:
CCF_TEST_SYNC_AFTER_SETUP: 1
CCF_LEAN_TRACE_VALIDATOR: ${{ github.workspace }}/lean/disaster-recovery-trace/.lake/build/bin/trace-validator
ELECTION_TIMEOUT_MS: 10000

- name: "Capture dmesg"
Expand All @@ -336,6 +352,7 @@ jobs:
build/workspace/*/out
build/workspace/*/err
build/workspace/*/*.ledger/*
build/workspace/**/*.recovery.ndjson
build/workspace/*/stack_trace
build/workspace/**/openapi_coverage.json
if-no-files-found: ignore
Expand Down Expand Up @@ -378,13 +395,28 @@ jobs:
python3 tests/infra/platform_detection.py snp genoa
shell: bash

- name: "Build Lean recovery trace validator"
run: |
set -euo pipefail
curl --proto '=https' --tlsv1.2 -sSf \
https://raw.githubusercontent.com/leanprover/elan/58e8d545e33641f66dbcbd22c4283109e71757be/elan-init.sh \
-o /tmp/elan-init.sh
sh /tmp/elan-init.sh -y --default-toolchain none
rm /tmp/elan-init.sh
export PATH="${HOME}/.elan/bin:${PATH}"
elan toolchain install "$(cat lean/disaster-recovery-trace/lean-toolchain)"
cd lean/disaster-recovery-trace
lake exe cache get
lake build trace-validator
shell: bash

- name: "Build Debug"
run: |
set -ex
git config --global --add safe.directory /__w/CCF/CCF
mkdir build
cd build
cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DWORKER_THREADS=1 ..
cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DWORKER_THREADS=1 -DCCF_RECOVERY_TRACE=ON ..
ninja
shell: bash

Expand All @@ -400,6 +432,7 @@ jobs:
shell: bash
env:
CCF_TEST_SYNC_AFTER_SETUP: 1
CCF_LEAN_TRACE_VALIDATOR: ${{ github.workspace }}/lean/disaster-recovery-trace/.lake/build/bin/trace-validator
ELECTION_TIMEOUT_MS: 10000

- name: "Capture dmesg"
Expand All @@ -422,6 +455,7 @@ jobs:
build/workspace/*/out
build/workspace/*/err
build/workspace/*/*.ledger/*
build/workspace/**/*.recovery.ndjson
build/workspace/*/stack_trace
build/workspace/**/openapi_coverage.json
if-no-files-found: ignore
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/lean.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ on:
pull_request:
paths:
- "lean/**"
- "include/ccf/service/tables/self_healing_open.h"
- "src/node/recovery_decision_protocol.cpp"
- "src/node/recovery_decision_protocol.h"
- "src/node/rpc/self_healing_open_handlers.h"
- "tests/e2e_operations.py"
- "tests/infra/recovery_trace.py"
- "tests/infra/recovery_trace_test.py"
- "CMakeLists.txt"
- ".github/workflows/ci.yml"
- ".github/workflows/lean.yml"

concurrency:
Expand Down Expand Up @@ -45,3 +54,43 @@ jobs:
lake build --wfail
lake lint
lake exe canonical-checks

trace-validator:
name: Trace validator
runs-on: ubuntu-26.04
timeout-minutes: 20

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install Lean
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y elan
elan toolchain install "$(cat lean/disaster-recovery-trace/lean-toolchain)"

- name: Restore Mathlib cache
working-directory: lean/disaster-recovery-trace
shell: bash
run: |
set -euo pipefail
lake exe cache get

- name: Build and test validator
working-directory: lean/disaster-recovery-trace
shell: bash
run: |
set -euo pipefail
lake exe mk_all --check --lib DisasterRecoveryTrace
lake build --wfail
lake lint
lake exe trace-checks

- name: Test trace extraction and ordering
working-directory: tests
shell: bash
run: |
set -euo pipefail
python3 -m unittest infra.recovery_trace_test
3 changes: 3 additions & 0 deletions lean/disaster-recovery-trace/DisasterRecoveryTrace.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DisasterRecoveryTrace.Protocol.Trace
import DisasterRecoveryTrace.Protocol.Trace.Format
import DisasterRecoveryTrace.Protocol.Trace.Replay
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import DisasterRecoveryTrace.Protocol.Trace.Format
import DisasterRecoveryTrace.Protocol.Trace.Replay
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import DisasterRecovery.Protocol.Model
import Lean.Data.Json
import Lean.Data.Json.FromToJson

namespace DisasterRecoveryTrace.Protocol.Trace

open DisasterRecovery.Protocol.Model

open Lean

def contractVersion : String :=
"ccf.recovery_decision_protocol.trace/1"

inductive Kind where
| start
| gossipAccepted
| voteAccepted
| iAmOpenAccepted
| timeout
| send
| open
| joinRestart
| complete
deriving Repr, BEq, Inhabited

structure TraceEvent where
version : String
instanceId : String
expectedLocations : List Location
node : Location
sequence : Nat
kind : Kind
messageId : Option String
causedBy : Option String
source : Option Location
txid : Option TxID
pre : Option Phase
post : Option Phase
openKind : Option OpenKind
send : Option String
deriving Repr, BEq, Inhabited

private def parseKind : String -> Except String Kind
| "start" => pure .start
| "gossip_accepted" => pure .gossipAccepted
| "vote_accepted" => pure .voteAccepted
| "iamopen_accepted" => pure .iAmOpenAccepted
| "timeout" => pure .timeout
| "send" => pure .send
| "open" => pure .open
| "join_restart" => pure .joinRestart
| "complete" => pure .complete
| value => throw s!"unknown kind '{value}'"

private def parsePhase : String -> Except String Phase
| "GOSSIPING" => pure .gossiping
| "VOTING" => pure .voting
| "OPENING" => pure .opening
| "JOINING" => pure .joining
| "OPEN" => pure .open
| value => throw s!"unknown phase '{value}'"

private def parseOpenKind : String -> Except String OpenKind
| "QUORUM" => pure .quorum
| "FAILOVER" => pure .failover
| value => throw s!"unknown open kind '{value}'"

private def optionalString (json : Json) (key : String) :
Except String (Option String) :=
match json.getObjVal? key with
| .error _ | .ok .null => pure none
| .ok value => some <$> value.getStr?

private def optionalNat (json : Json) (key : String) :
Except String (Option Nat) :=
match json.getObjVal? key with
| .error _ | .ok .null => pure none
| .ok value => some <$> value.getNat?

private def optionalParsed
(json : Json)
(key : String)
(parse : String -> Except String α) :
Except String (Option α) := do
match <- optionalString json key with
| none => pure none
| some value => some <$> parse value

def parseEvent (line : String) : Except String TraceEvent := do
let json <- Json.parse line
let version <- json.getObjValAs? String "version"
if version != contractVersion then
throw s!"unsupported version '{version}'"

let view <- optionalNat json "view"
let seqno <- optionalNat json "seqno"
if view.isSome != seqno.isSome then
throw "view and seqno must appear together"

let instanceId <- json.getObjValAs? String "instance"
let expectedLocations <-
json.getObjValAs? (List String) "expected_locations"
let node <- json.getObjValAs? String "node"
let sequence <- json.getObjValAs? Nat "sequence"
let kindName <- json.getObjValAs? String "kind"
let kind <- parseKind kindName
let messageId <- optionalString json "message_id"
let causedBy <- optionalString json "caused_by"
let source <- optionalString json "source"
let pre <- optionalParsed json "pre" parsePhase
let post <- optionalParsed json "post" parsePhase
let openKind <- optionalParsed json "open_kind" parseOpenKind
let send <- optionalString json "send"
pure {
version
instanceId
expectedLocations
node
sequence
kind
messageId
causedBy
source
txid := match view, seqno with
| some view, some seqno => some { view, seqno }
| _, _ => none
pre
post
openKind
send
}

def parseNDJSON (input : String) : Except String (List TraceEvent) := do
let lines := (input.splitOn "\n").filter
(fun line => !line.trimAscii.isEmpty)
let mut events := []
for (line, index) in lines.zipIdx do
match parseEvent line with
| .ok event => events := event :: events
| .error message => throw s!"line {index + 1}: {message}"
pure events.reverse

end DisasterRecoveryTrace.Protocol.Trace
Loading