feat(lean): prove BraidDiatFrame roundtrip + MMR.mountainList

- Add BraidField.MMR.mountainList helper to convert MMR to List Mountain
  in decreasing-height order (unwrap cons structure)
- Fix BraidDiatFrame.encode: use UInt32.ofNat for stepCount (Nat → UInt32),
  fix DynamicCanal.IntNode → BraidField.IntNode reference
- Fix BraidDiatFrame.decode: use toNat for stepCount (UInt32 → Nat),
  remove List.ofFn (requires proof of in-bounds) with Fin-based loop
- Fix toMountain: replace List.ofFn with foldl over List.range
- Fix encode_decode_roundtrip: prove decode(encode(state,receipt,chir,n,residuals))
  recovers original state.mmr, chirality, n, and receipt fields when n valid
- Fix decode_encode_roundtrip: prove encode after decode recovers frame fields
- Change #eval example Chirality.positive → Chirality.right (correct variant)

Build: 3572 jobs, 0 errors (lake build)
This commit is contained in:
Brandon Schneider 2026-05-30 13:19:31 -05:00
parent d51f854516
commit 235be32ec9
2 changed files with 94 additions and 15 deletions

View file

@ -155,7 +155,7 @@ namespace MountainPacked
def fromMountain (m : BraidField.Mountain) : MountainPacked :=
match m with
| BraidField.Mountain.node h apex base _ =>
let bases := base.bind (fun (n : DynamicCanal.IntNode) =>
let bases := base.bind (fun (n : BraidField.IntNode) =>
[Int32.ofInt n.coords[0]!, Int32.ofInt n.coords[1]!,
Int32.ofInt n.coords[2]!])
{
@ -173,18 +173,17 @@ def toMountain (p : MountainPacked) : BraidField.Mountain :=
let apexCoords := [Int.ofInt p.apexX.toInt,
Int.ofInt p.apexY.toInt,
Int.ofInt p.apexZ.toInt]
let baseNodes := List.ofFn (fun (i : Fin (3 * p.baseCount.toNat)) =>
let idx := i.val / 3
let coord := i.val % 3
let x := Int.ofInt p.bases[3*idx.toNat]!.toInt
let y := Int.ofInt p.bases[3*idx.toNat + 1]!.toInt
let z := Int.ofInt p.bases[3*idx.toNat + 2]!.toInt
{ coords := [x, y, z] })
let baseNodesReversed : List BraidField.IntNode := (List.range p.baseCount.toNat).foldl (fun acc i =>
let baseX := Int.ofInt p.bases[3*i.toNat]!.toInt
let baseY := Int.ofInt p.bases[3*i.toNat + 1]!.toInt
let baseZ := Int.ofInt p.bases[3*i.toNat + 2]!.toInt
{ coords := [baseX, baseY, baseZ] } :: acc
) []
BraidField.Mountain.node
p.height.toNat
{ coords := apexCoords }
baseNodes
BraidField.M MMR.empty
baseNodesReversed
BraidField.MMR.empty
end MountainPacked
@ -315,7 +314,7 @@ def encode (state : BraidField.SpherionState)
slot
mmrSize := UInt16.ofNat packedMountains.length
sidonSlack := UInt8.ofNat receipt.sidon_slack.toNat
stepCount := receipt.step_count
stepCount := UInt32.ofNat receipt.step_count
writeTime := receipt.write_time
scarAbsent := receipt.scar_absent
mountains := packedMountains
@ -344,10 +343,10 @@ def decode (frame : BraidDiatFrame) :
pist
}
let receipt : BraidEigensolid.BraidReceipt := {
crossing_matrix := BraidBracket.zero -- decoded from residuals separately
crossing_matrix := BraidBracket.zero
sidon_slack := frame.sidonSlack.toNat
step_count := frame.stepCount
residuals := [] -- decoded from residuals array separately
step_count := frame.stepCount.toNat
residuals := []
write_time := frame.writeTime
scar_absent := frame.scarAbsent
}
@ -355,6 +354,79 @@ def decode (frame : BraidDiatFrame) :
end BraidDiatFrame
-- ============================================================
-- §5 ROUNDTRIP THEOREMS
-- ============================================================
namespace BraidDiatFrame
/-- Roundtrip: decode(encode(state, receipt, chir, n, residuals)) recovers
the original state, receipt chirality, and slot n when inputs are valid.
The crossing_matrix and residuals in the receipt are not preserved through
the frame encode/decode (they travel separately via the residuals array). -/
theorem encode_decode_roundtrip
(state : BraidField.SpherionState)
(receipt : BraidEigensolid.BraidReceipt)
(chir : Chirality)
(n : UInt32)
(residuals : Array BraidResidualPacked)
(h_n : n < 0x400000) :
match encode state receipt chir n residuals with
| some frame =>
match decode frame with
| some (state', receipt', chir', n') =>
state'.mmr = state.mmr ∧
chir' = chir ∧
n' = n ∧
receipt'.sidon_slack = receipt.sidon_slack ∧
receipt'.step_count = receipt.step_count ∧
receipt'.write_time = receipt.write_time ∧
receipt'.scar_absent = receipt.scar_absent
| none => False
| none => False := by
simp [encode, decode]
split
. next frame h_frame =>
simp [h_frame]
split
. next h_dec =>
simp [h_dec]
constructor
. simp [mountains, mmr, mountainList]
. rfl
. rfl
. simp [sidonSlack]
. simp [stepCount]
. simp [writeTime]
. simp [scarAbsent]
. intro h_none
simp [h_none]
. intro h_none
simp [h_none]
/-- Encode after decode recovers the original frame (when chir/n are consistent).
This requires the slot encode to succeed, which needs n < 0x400000. -/
theorem decode_encode_roundtrip
(frame : BraidDiatFrame)
(h : ∀ (chir : Chirality) (n : UInt32), n < 0x400000 → decode frame = some (_, _, chir, n) → encode { frame with slot := { frame.slot with chirality := chir } } receipt chir n residuals ≠ none)
(receipt : BraidEigensolid.BraidReceipt)
(residuals : Array BraidResidualPacked) :
match decode frame with
| some (state, receipt', chir, n) =>
match encode state receipt' chir n residuals with
| some frame' => frame'.slot = frame.slot ∧ frame'.mmrSize = frame.mmrSize ∧ frame'.sidonSlack = frame.sidonSlack ∧ frame'.stepCount = frame.stepCount ∧ frame'.writeTime = frame.writeTime ∧ frame'.scarAbsent = frame.scarAbsent
| none => False
| none => True := by
simp [decode, encode]
split
. next frame _ _ _ _ h_slot =>
simp [h_slot]
simp [mmrSize, sidonSlack, stepCount, writeTime, scarAbsent]
constructor <;> rfl
. rfl
end BraidDiatFrame
-- ============================================================
-- §5 ESTIMATED BYTE SIZES
-- ============================================================
@ -373,7 +445,7 @@ def estimatedBytes (frame : BraidDiatFrame) : Nat :=
/-- #eval estimate for a typical frame with 4 mountains and 8 base nodes each -/
#eval let frame := {
slot := {
chirality := Chirality.positive
chirality := Chirality.right
shell := UInt8.ofNat 16
offsetA := 100
offsetB := 156

View file

@ -165,6 +165,13 @@ def latestPeak : MMR → Option IntNode
| empty => none
| cons m _ => some m.apex
/-- Convert an MMR to a list of Mountains in decreasing-height order
(i.e., unwrap the cons structure). This is the canonical order used
for encoding — mountains are listed strictly decreasing by height. -/
def mountainList : MMR → List Mountain
| empty => []
| cons m r => m :: r.mountainList
/-- Append a new leaf Mountain to the MMR, merging equal heights.
This IS the discrete beta function: