import ApcOptimizer.Spec import ApcOptimizer.MemoryBus set_option autoImplicit false namespace ApcOptimizer.SP1 variable {p : ℕ} /-- The SP1 bus types that appear in the default bus map. -/ inductive Sp1BusType where /-- The CPU state ("execution bridge"): an instruction receives the current `(clk, pc)` and sends the next one. Payload: `(clk... (2 fields), pc... (3 fields))`. -/ | executionBridge /-- To access a memory cell, instructions send the previous record and receive the new one. Payload: `(clk... (2 fields), address... (3 limbs), data... (4 × 16-bit limbs))`. -/ | memory /-- Program lookup: fetch the instruction at the current pc. Payload has 16 fields. -/ | pcLookup /-- Byte lookup. Format `(op, a, b, c)`, where `op` selects the operation on bytes `b`, `c`: 0 `AND` (`a = b & c`), 1 `OR`, 2 `XOR`, 3 `U8Range` (`a = 0`), 4 `LTU` (`a = [b < c]`), 5 `MSB` (`a = b >> 7`, `c = 0`), 6 `Range` (`a < 2^b`, `c = 0`, `b ≤ 16`). -/ | byteLookup /-- Instruction-fetch lookup (the untrusted instruction table). Payload has 22 fields. -/ | instructionFetch /-- Page-protection lookup. Payload has 6 fields. -/ | pageProt deriving Repr, DecidableEq /-- A mapping from bus IDs to bus type. -/ abbrev BusMap := Nat → Option Sp1BusType /-- A concrete bus map as parsed from a powdr export's `bus_map.bus_ids` field: an association list bus id ↦ bus type. -/ abbrev BusMapList := List (Nat × Sp1BusType) /-- Convert a `BusMapList` to a `BusMap` lookup function. -/ def BusMapList.toBusMap (busMap : BusMapList) : BusMap := fun busId => busMap.lookup busId /-- The hard-coded default SP1 bus map, mirroring powdr's `sp1_bus_map`. -/ def defaultBusMap : BusMap | 1 => some .memory | 2 => some .pcLookup | 5 => some .byteLookup | 7 => some .executionBridge | 16 => some .instructionFetch | 18 => some .pageProt | _ => none /-- Stateful buses are the execution bridge and memory; the rest are stateless lookups. -/ def Sp1BusType.isStateful : Sp1BusType → Bool | .executionBridge => true | .memory => true | .pcLookup => false | .byteLookup => false | .instructionFetch => false | .pageProt => false /-- Whether a field element is a byte (`0 ≤ x < 256`). -/ def isByte (x : ZMod p) : Prop := x.val < 256 /-- Whether a field element is a 16-bit limb (`0 ≤ x < 2^16`). -/ def is16Bit (x : ZMod p) : Prop := x.val < 2 ^ 16 /-- The named fields of an SP1 memory payload, `(clk… (2 fields), address… (3 limbs), data… (4 × 16-bit limbs))`. -/ structure MemoryPayload (p : ℕ) where /-- The two clock fields. -/ clk : Vector (ZMod p) 2 /-- The three address limbs. -/ address : Vector (ZMod p) 3 /-- The four limbs of the memory word. -/ data : Vector (ZMod p) 4 /-- Read a memory payload's named fields; `none` if the number of entries is wrong. -/ def memoryPayload? : List (ZMod p) → Option (MemoryPayload p) | c0 :: c1 :: a0 :: a1 :: a2 :: d0 :: d1 :: d2 :: d3 :: _ => some { clk := #v[c0, c1], address := #v[a0, a1, a2], data := #v[d0, d1, d2, d3] } | _ => none /-- For lookups, whether the message is in the lookup table. -/ def accepts (busMap : BusMap) (msg : BusInteraction (ZMod p)) : Prop := match busMap msg.busId, msg.payload with -- As for OpenVM's PC lookup, we only check the arity of these lookups: the input circuit has -- already fixed the looked-up fields to constants, so the optimizer cannot change them. | some .pcLookup, args => args.length = 16 | some .instructionFetch, args => args.length = 22 | some .pageProt, args => args.length = 6 | some .byteLookup, [op, a, b, c] => match op.val with -- AND: `b`, `c` are bytes and `a = b & c`. | 0 => isByte b ∧ isByte c ∧ a.val = Nat.land b.val c.val -- OR: `a = b | c`. | 1 => isByte b ∧ isByte c ∧ a.val = Nat.lor b.val c.val -- XOR: `a = b ^ c`. | 2 => isByte b ∧ isByte c ∧ a.val = Nat.xor b.val c.val -- U8Range: `b`, `c` are bytes and `a = 0`. | 3 => isByte b ∧ isByte c ∧ a.val = 0 -- LTU: `a` is 1 if `b < c`, else 0. | 4 => isByte b ∧ isByte c ∧ a.val = if b.val < c.val then 1 else 0 -- MSB: `a` is the top bit of the byte `b`, and `c = 0`. | 5 => isByte b ∧ c.val = 0 ∧ a.val = b.val >>> 7 -- Range: `a < 2^b` with `c = 0` and `b ≤ 16` (the largest width the table holds). | 6 => b.val ≤ 16 ∧ c.val = 0 ∧ a.val < 2 ^ b.val -- No other op is in the table. | _ => False | some .byteLookup, _ => False -- Stateful buses have no table to contradict. | some .executionBridge, _ => True -- In SP1, the invariant is that memory data limbs are 16-bit range-checked. A *send* -- (multiplicity 1) reads the previous record, so its data must be 16-bit. | some .memory, payload => match memoryPayload? payload with | some f => msg.multiplicity = 1 → ∀ d ∈ f.data, is16Bit d | none => True -- Invalid bus ID. Won't have a matching receive. | none, _ => False /-- Whether a message maintains the invariants on which soundness depends. Only called for messages with nonzero multiplicity. -/ def maintainsInvariants (busMap : BusMap) (msg : BusInteraction (ZMod p)) : Prop := match busMap msg.busId with -- Lookups are only ever sent (multiplicity 1). | some .pcLookup | some .byteLookup | some .instructionFetch | some .pageProt => msg.multiplicity = 1 -- The execution bridge is stateful: it is sent (1) or received (-1). | some .executionBridge => msg.multiplicity = 1 ∨ msg.multiplicity = -1 -- Memory is stateful (multiplicity 1 or -1), and additionally maintains the invariant that its -- data limbs are 16-bit range. | some .memory => (msg.multiplicity = 1 ∨ msg.multiplicity = -1) ∧ (match memoryPayload? msg.payload with | some f => ∀ d ∈ f.data, is16Bit d | none => True) -- Circuits should not send messages to an unknown bus. | none => False /-- Assume that reading register `x0` (address `0`) always returns `0`. This should be enforced globally by all SP1 chips. -/ def x0ReturnsZero (busMap : BusMap) (msgs : List (BusInteraction (ZMod p))) : Prop := ∀ m ∈ msgs, busMap m.busId = some .memory → -- If all three address limbs are 0 (register x0), then the four data limbs must be 0. m.payload[2]? = some 0 → m.payload[3]? = some 0 → m.payload[4]? = some 0 → m.payload[5]? = some 0 ∧ m.payload[6]? = some 0 ∧ m.payload[7]? = some 0 ∧ m.payload[8]? = some 0 /-- Maps a bus ID to its memory bus shape, if applicable. For SP1 *memory*, the `getPrevious` sends the previous record and the `setNew` receives the new one, so `direction := .sendThenReceive` (`setNewMult = -1`) — the reverse of OpenVM. The *execution bridge* is different: like every VM's, an instruction sends the next CPU state and receives the current one, so its `setNew` (the next state) is the send — `direction := .receiveThenSend` (`setNewMult = 1`), matching OpenVM's execution bridge and OpenVM memory, *not* SP1 memory. -/ def memShapeOf (busMap : BusMap) (busId : Nat) : Option MemoryBusShape := match busMap busId with -- The *actual* memory bus, with the three address limbs in payload slots 2, 3 and 4. | some .memory => some { addressFields := [2, 3, 4], direction := .sendThenReceive } -- The execution bridge is a single-global-cell (address `[]`) memory bus that sends the *next* -- CPU state. As in OpenVM, this makes completeness partial: we assume the prover always proves -- consecutive cycles. | some .executionBridge => some { addressFields := [], direction := .receiveThenSend } | _ => none /-- The SP1 bus semantics for a given bus map (default: the hard-coded default bus map). -/ def sp1BusSemantics (p : ℕ) (busMap : BusMap := defaultBusMap) : BusSemantics p where isStateful busId := match busMap busId with | some t => t.isStateful | none => false accepts := accepts busMap maintainsInvariants := maintainsInvariants busMap -- The memory discipline, per declared bus. On SP1 *memory* the `setNew` multiplicity is `-1` -- (`direction := .sendThenReceive`); on the *execution bridge* it is `1` (`.receiveThenSend`, which -- sends the next CPU state). Either way `admissibleMemoryBus` pairs each `setNew` with the next -- same-address `getPrevious` directly in list order. admissible msgs := (∀ (busId : Nat) (shape : MemoryBusShape), memShapeOf busMap busId = some shape → admissibleMemoryBus shape (msgs.filter (fun m => m.busId = busId))) ∧ x0ReturnsZero busMap msgs /-- SP1's proving-backend degree bound (powdr's `DEFAULT_DEGREE_BOUND` for SP1), used when the optimizer is run directly rather than with a bound passed in over the FFI. -/ def defaultDegreeBound : DegreeBound := { identities := 3, busInteractions := 1 } /-- The KoalaBear field modulus, `2^31 - 2^24 + 1` — the field all powdr SP1 exports use. -/ def koalaBear : Nat := 2130706433 instance : NeZero koalaBear := ⟨by decide⟩ end ApcOptimizer.SP1