-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathLayoutReport.lean
More file actions
303 lines (272 loc) · 13 KB
/
Copy pathLayoutReport.lean
File metadata and controls
303 lines (272 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import Compiler.CompilationModel.Types
import Compiler.CompilationModel.AbiHelpers
import Compiler.CompilationModel.LayoutValidation
import Compiler.Json
namespace Compiler.CompilationModel
open Compiler.Json
private def jsonNat (n : Nat) : String :=
toString n
private def jsonOption (render : α → String) : Option α → String
| some value => render value
| none => "null"
private def mappingKeyTypeString : MappingKeyType → String
| .address => "address"
| .uint256 => "uint256"
| .bytes32 => "bytes32"
private def structMemberTypeString : StructMemberType → String
| .uint256 => "uint256"
| .uint16 => "uint16"
| .address => "address"
| .bool => "bool"
| .bytes32 => "bytes32"
private def mappingKeysJson (keys : List MappingKeyType) : String :=
jsonArray (keys.map (fun keyType => jsonString (mappingKeyTypeString keyType)))
private def packedBitsJson (packed : PackedBits) : String :=
jsonObject [
("offset", jsonNat packed.offset),
("width", jsonNat packed.width)
]
private def structMemberJson (member : StructMember) : String :=
jsonObject [
("name", jsonString member.name),
("type", jsonString (structMemberTypeString member.ty)),
("wordOffset", jsonNat member.wordOffset),
("packedBits", jsonOption packedBitsJson member.packed)
]
private def fieldTypeJson : FieldType → String
| .uint256 =>
jsonObject [("kind", jsonString "uint256")]
| .address =>
jsonObject [("kind", jsonString "address")]
| .adt name maxFields =>
jsonObject [
("kind", jsonString "adt"),
("name", jsonString name),
("maxFields", jsonNat maxFields)
]
| .dynamicArray elemType =>
jsonObject [
("kind", jsonString "dynamicArray"),
("elemType", jsonString (paramTypeToSolidityString (storageArrayElemTypeToParamType elemType)))
]
| .mappingTyped mt =>
jsonObject [
("kind", jsonString "mapping"),
("keys", mappingKeysJson (mappingTypeKeyTypes mt)),
("valueKind", jsonString "uint256")
]
| .mappingStruct keyType members =>
jsonObject [
("kind", jsonString "mappingStruct"),
("keys", jsonArray [jsonString (mappingKeyTypeString keyType)]),
("members", jsonArray (members.map structMemberJson))
]
| .mappingStruct2 outer inner members =>
jsonObject [
("kind", jsonString "mappingStruct"),
("keys", jsonArray [jsonString (mappingKeyTypeString outer), jsonString (mappingKeyTypeString inner)]),
("members", jsonArray (members.map structMemberJson))
]
private def reservedSlotRangeJson (range : ReservedSlotRange) : String :=
jsonObject [
("start", jsonNat range.start),
("end", jsonNat range.end_)
]
private def slotAliasRangeJson (range : SlotAliasRange) : String :=
jsonObject [
("sourceStart", jsonNat range.sourceStart),
("sourceEnd", jsonNat range.sourceEnd),
("targetStart", jsonNat range.targetStart)
]
private def fieldJson (declaredField effectiveField : Field) (idx : Nat) : String :=
let canonicalSlot := declaredField.slot.getD idx
let effectiveAliasSlots := effectiveField.aliasSlots
jsonObject [
("name", jsonString declaredField.name),
("declaredSlot", jsonOption jsonNat declaredField.slot),
("canonicalSlot", jsonNat canonicalSlot),
("declaredAliasSlots", jsonArray (declaredField.aliasSlots.map jsonNat)),
("effectiveAliasSlots", jsonArray (effectiveAliasSlots.map jsonNat)),
("writeSlots", jsonArray ((canonicalSlot :: effectiveAliasSlots).map jsonNat)),
("type", fieldTypeJson declaredField.ty),
("packedBits", jsonOption packedBitsJson declaredField.packedBits)
]
private def immutableJson (imm : ImmutableSpec) : String :=
jsonObject [
("name", jsonString imm.name),
("type", jsonString (paramTypeToSolidityString imm.ty)),
("storageFootprint", jsonNat 0),
("kind", jsonString "bytecodeImmutable")
]
/-! ### Storage families and non-alias certificate (#1966)
Each storage field becomes a "family" — the set of storage locations the
field can occupy at runtime. Plain scalars occupy a single declared slot;
mapping families occupy keccak-derived locations parameterised by their
key shape; nested mapping families nest a second keccak; mapping-struct
families add a bounded word offset to the keccak base. The certificate
records the *derivation kind*, the *root slot*, the *keccak preimage
shape* (when applicable), and any *struct-offset range*, so a downstream
proof obligation can replace the global keccak-injectivity boundary with
local per-family obligations and machine-checkable claims for the
finite, declared subset. -/
private def familyKindString : FieldType → String
| .uint256 | .address | .adt _ _ => "scalar"
| .dynamicArray _ => "dynamicArray"
| .mappingTyped mt =>
if (mappingTypeKeyTypes mt).length ≥ 2 then "nestedMapping" else "mapping"
| .mappingStruct _ _ => "mappingStruct"
| .mappingStruct2 _ _ _ => "nestedMappingStruct"
/-- Symbolic keccak preimage description used as the per-family
non-collision justification. `null` for plain scalars, where
non-alias reduces to declared-slot distinctness. -/
private def familyKeccakPreimage : FieldType → Nat → String
| .uint256, _ | .address, _ | .adt _ _, _ => "null"
| .dynamicArray _, slot =>
-- elements at keccak256(rootSlot) + i
jsonString s!"keccak256(slot={slot})"
| .mappingTyped mt, slot =>
let keys := mappingTypeKeyTypes mt
let keyStr := String.intercalate ", " (keys.map mappingKeyTypeString)
if keys.length ≥ 2 then
jsonString s!"keccak256(innerKey || keccak256(outerKey || slot={slot})) [keys: {keyStr}]"
else
jsonString s!"keccak256(key || slot={slot}) [keys: {keyStr}]"
| .mappingStruct keyType _, slot =>
jsonString s!"keccak256(key || slot={slot}) + wordOffset [keys: {mappingKeyTypeString keyType}]"
| .mappingStruct2 outerKey innerKey _, slot =>
jsonString s!"keccak256(innerKey || keccak256(outerKey || slot={slot})) + wordOffset [keys: {mappingKeyTypeString outerKey}, {mappingKeyTypeString innerKey}]"
/-- Maximum struct word offset for mapping-struct families, or `null`. -/
private def familyStructWordRange : FieldType → String
| .mappingStruct _ members | .mappingStruct2 _ _ members =>
let maxOffset := members.foldl (fun acc m => Nat.max acc m.wordOffset) 0
jsonObject [("min", jsonNat 0), ("maxInclusive", jsonNat maxOffset)]
| _ => "null"
private def storageFamilyJson (declaredField : Field) (idx : Nat) : String :=
let canonicalSlot := declaredField.slot.getD idx
jsonObject [
("name", jsonString declaredField.name),
("kind", jsonString (familyKindString declaredField.ty)),
("rootSlot", jsonNat canonicalSlot),
("keccakPreimage", familyKeccakPreimage declaredField.ty canonicalSlot),
("structWordRange", familyStructWordRange declaredField.ty)
]
/-- Pairwise non-alias claim: an unordered pair of family names paired
with a *justification* the obligation discharger should use:
- `distinctScalarSlots` — both are scalar families whose *effective
write slot sets* (canonical slot ∪ `aliasSlots` ∪
`slotAliasRanges`-derived aliases) are disjoint; the claim is a
closed-form finite decision the proof side discharges with `decide`.
- `writeSetsOverlap` — both are scalar families but their
effective write slot sets intersect; this is a real aliasing
conflict the certificate must surface rather than silently assert
`distinctScalarSlots` for (Bugbot #1967).
- `keccakDomainScalar` — one is keccak-derived, the other a scalar
at a declared slot < 2^32 (say); the claim assumes keccak digest >
maxDeclaredSlot, which is a standard preimage assumption.
- `keccakPreimageDistinct` — both are keccak-derived families; the
claim assumes keccak256 is injective on the disjoint preimage
shapes (the per-family `keccakPreimage` strings differ).
- `sameFamilyDistinctKey` — same family, different keys: assumes
keccak256 injectivity on keys.
The compiler-side artifact emits the claims as data; the proof side
discharges them via either a `decide`/`native_decide` lemma (for the
finite scalar subset) or a named local keccak assumption. -/
private def isKeccakDerivedFamily : FieldType → Bool
| .uint256 | .address | .adt _ _ => false
| _ => true
/-- Effective scalar write slot set for a single field: the declared/derived
canonical slot union the field's own `aliasSlots` and any
`slotAliasRanges`-derived aliases. This is the set of storage words the
field *actually writes to* at runtime, so it is the set the pairwise
non-alias certificate must compare against. -/
private def effectiveScalarWriteSlots
(f : Field) (idx : Nat) (aliasRanges : List SlotAliasRange) : List Nat :=
let canonical := f.slot.getD idx
let mergedAliases := dedupNatPreserve
(f.aliasSlots ++ derivedAliasSlotsForSource canonical aliasRanges)
dedupNatPreserve (canonical :: mergedAliases)
/-- Predicate: do two scalar fields share any effective write slot?
Two scalars only truly non-alias when their effective write slot sets
are disjoint; the declared slots alone are insufficient because
`aliasSlots` and `slotAliasRanges` can still fold them onto the same
canonical word. -/
private def scalarWriteSetsOverlap
(a b : Field) (idxA idxB : Nat) (aliasRanges : List SlotAliasRange) : Bool :=
let writeA := effectiveScalarWriteSlots a idxA aliasRanges
let writeB := effectiveScalarWriteSlots b idxB aliasRanges
writeA.any (fun s => writeB.contains s)
/-- Justification for a pairwise non-alias claim. Both the family kinds and
the effective scalar write sets feed into the choice: when two scalar
families have overlapping write slot sets the certificate must report a
real aliasing conflict (`writeSetsOverlap`) rather than assert a
decidable `distinctScalarSlots` claim the proof side would happily
discharge. -/
private def nonAliasJustification
(a b : Field) (idxA idxB : Nat) (aliasRanges : List SlotAliasRange) : String :=
let aKeccak := isKeccakDerivedFamily a.ty
let bKeccak := isKeccakDerivedFamily b.ty
if !aKeccak && !bKeccak then
if scalarWriteSetsOverlap a b idxA idxB aliasRanges then
"writeSetsOverlap"
else
"distinctScalarSlots"
else if aKeccak && bKeccak then "keccakPreimageDistinct"
else "keccakDomainScalar"
private def nonAliasClaimJson (a b : Field) (idxA idxB : Nat)
(aliasRanges : List SlotAliasRange) : String :=
let slotA := a.slot.getD idxA
let slotB := b.slot.getD idxB
let writeA := effectiveScalarWriteSlots a idxA aliasRanges
let writeB := effectiveScalarWriteSlots b idxB aliasRanges
jsonObject [
("a", jsonString a.name),
("b", jsonString b.name),
("aSlot", jsonNat slotA),
("bSlot", jsonNat slotB),
("aWriteSlots", jsonArray (writeA.map jsonNat)),
("bWriteSlots", jsonArray (writeB.map jsonNat)),
("justification", jsonString (nonAliasJustification a b idxA idxB aliasRanges))
]
/-- Build the list of unordered pairwise non-alias claims for a contract. -/
private def nonAliasClaimsJson (fields : List Field)
(aliasRanges : List SlotAliasRange) : List String :=
let indexed := fields.zipIdx
let rec go : List (Field × Nat) → List (Field × Nat) → List String
| [], _ => []
| (a, ai) :: rest, _all =>
let here := rest.map (fun (b, bi) => nonAliasClaimJson a b ai bi aliasRanges)
here ++ go rest _all
go indexed indexed
/-- Render a machine-readable storage layout report for upgrade/layout auditing.
Includes the per-contract `storageFamilies` and `nonAliasClaims` sections
(#1966) so reviewers can replace a global keccak-injectivity boundary
with per-family local obligations and decide the declared-scalar subset
by finite case analysis. -/
def emitLayoutReportJson (specs : List CompilationModel) : String :=
jsonObject [
("contracts", jsonArray (specs.map contractJson))
]
where
contractJson (spec : CompilationModel) : String :=
let effectiveFields := applySlotAliasRanges spec.fields spec.slotAliasRanges
let fieldsJson :=
(spec.fields.zip effectiveFields).zipIdx.map fun ((declaredField, effectiveField), idx) =>
fieldJson declaredField effectiveField idx
let familiesJson :=
spec.fields.zipIdx.map fun (declaredField, idx) =>
storageFamilyJson declaredField idx
let claimsJson := nonAliasClaimsJson spec.fields spec.slotAliasRanges
let nsField := match spec.storageNamespace with
| some ns => jsonString (toString ns)
| none => "null"
jsonObject [
("contract", jsonString spec.name),
("storageNamespace", nsField),
("fields", jsonArray fieldsJson),
("immutables", jsonArray (spec.immutables.map immutableJson)),
("storageFamilies", jsonArray familiesJson),
("nonAliasClaims", jsonArray claimsJson),
("reservedSlotRanges", jsonArray (spec.reservedSlotRanges.map reservedSlotRangeJson)),
("slotAliasRanges", jsonArray (spec.slotAliasRanges.map slotAliasRangeJson))
]
end Compiler.CompilationModel