-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengine_api.qnt
More file actions
328 lines (285 loc) · 11.5 KB
/
engine_api.qnt
File metadata and controls
328 lines (285 loc) · 11.5 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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// Engine API specification in Quint
//
// Models the CL/EL interaction protocol from the CL's perspective.
// Reference: https://github.com/ethereum/execution-apis/tree/main/src/engine
//
// To run:
// quint run engine_api.qnt
// quint test engine_api.qnt
module engine_api {
// ==========================================================================
// TYPES
// ==========================================================================
// Block hashes are the primary identifiers
type BlockHash = str
// Payload IDs are tickets to retrieve built blocks
type PayloadId = str
// Possible responses from EL to forkchoice/newPayload calls
type PayloadStatus =
| Valid
| Invalid
| Syncing
| Accepted // for newPayload only
// ==========================================================================
// STATE
// ==========================================================================
// CL's view of the canonical chain head
var cl_head: BlockHash
// EL's set of validated block hashes (the "validated payloads pool")
var el_validated: Set[BlockHash]
// EL's current canonical head
var el_head: BlockHash
// Payloads currently being built: payload_id -> parent_hash
var el_building: PayloadId -> BlockHash
// Built payloads ready for retrieval: payload_id -> block_hash
var el_built: PayloadId -> BlockHash
// Last response from EL (for property checking)
var last_response: PayloadStatus
// ==========================================================================
// CONSTANTS
// ==========================================================================
pure val GENESIS: BlockHash = "genesis"
pure val BLOCK1: BlockHash = "block1"
pure val BLOCK2: BlockHash = "block2"
pure val BLOCK3: BlockHash = "block3"
pure val PID1: PayloadId = "pid1"
pure val PID2: PayloadId = "pid2"
// ==========================================================================
// INITIALIZATION
// ==========================================================================
action init = all {
cl_head' = GENESIS,
el_head' = GENESIS,
el_validated' = Set(GENESIS),
el_building' = Map(),
el_built' = Map(),
last_response' = Valid,
}
// ==========================================================================
// ACTIONS
// ==========================================================================
/// ForkchoiceUpdated without payload attributes
///
/// CL tells EL: "make this block the canonical head"
///
/// Maps to Inner::set_latest_forkchoice_state
///
/// The EL will:
/// - Retrieve the pre-validated block from its pool
/// - Promote the stored state to canonical state
/// - Update the canonical chain head pointer
action forkchoiceUpdated_setHead(block_hash: BlockHash): bool = all {
el_validated.contains(block_hash), // precondition: block must be validated by EL
cl_head' = block_hash, // CL updates its view of head
el_head' = block_hash, // EL updates canonical head
last_response' = Valid, // response is VALID
el_validated' = el_validated, // no changes to pools
el_building' = el_building,
el_built' = el_built,
}
/// ForkchoiceUpdated with unknown block hash
///
/// EL returns SYNCING when it doesn't know the block.
/// Maps to Inner::set_latest_forkchoice_state (SYNCING branch)
action forkchoiceUpdated_unknown(block_hash: BlockHash): bool = all {
not(el_validated.contains(block_hash)), // precondition: block is NOT in validated set
last_response' = Syncing, // EL responds SYNCING
cl_head' = cl_head, // no state changes
el_head' = el_head,
el_validated' = el_validated,
el_building' = el_building,
el_built' = el_built,
}
/// ForkchoiceUpdated with payload attributes
///
/// CL tells EL: "build me a new block on top of this parent"
///
/// Maps to Inner::generate_block
///
/// The EL will:
/// - Set working state to parent_hash
/// - Start collecting transactions from mempool
/// - Return a payload_id (ticket to retrieve the built block)
action forkchoiceUpdated_buildBlock(parent_hash: BlockHash, payload_id: PayloadId): bool = all {
el_validated.contains(parent_hash), // precondition: parent must be validated
not(el_building.keys().contains(payload_id)), // precondition: payload_id must not be in use
not(el_built.keys().contains(payload_id)),
el_building' = el_building.put(payload_id, parent_hash), // EL starts building
el_head' = parent_hash, // head updates to parent (per Engine API spec)
cl_head' = parent_hash,
last_response' = Valid, // response is VALID with payload_id
el_validated' = el_validated, // no change to other state
el_built' = el_built,
}
/// EL finishes building a block (internal EL action)
///
/// This models the async nature - block building takes time.
/// Once complete, the block moves from building to built pool.
action el_finishBuilding(payload_id: PayloadId, new_block_hash: BlockHash): bool = all {
el_building.keys().contains(payload_id), // precondition: must be currently building this payload
not(el_validated.contains(new_block_hash)), // precondition: new block hash must be fresh
el_built' = el_built.put(payload_id, new_block_hash), // move from building to built
el_building' = el_building.keys().exclude(Set(payload_id)).mapBy(k => el_building.get(k)),
el_validated' = el_validated.union(Set(new_block_hash)), // block is implicitly validated since EL built it
cl_head' = cl_head, // no change to heads or response
el_head' = el_head,
last_response' = last_response,
}
/// GetPayload
///
/// CL retrieves a built block by payload_id.
///
/// Maps to EngineAPI::get_payload
///
/// This is a read-only operation - CL gets the ExecutionPayloadV3.
action getPayload(payload_id: PayloadId): bool = all {
el_built.keys().contains(payload_id), // precondition: block must be built
cl_head' = cl_head, // read-only: no state changes
el_head' = el_head,
el_validated' = el_validated,
el_building' = el_building,
el_built' = el_built,
last_response' = last_response,
}
/// NewPayload - valid block
///
/// CL sends a block to EL for validation.
///
/// Maps to Inner::notify_new_block
///
/// The EL will:
/// - Set working state to parent_hash
/// - Execute transactions from the payload
/// - Compute state and verify against payload's state_root
/// - Store in "validated payloads pool" indexed by block_hash
action newPayload_valid(block_hash: BlockHash, parent_hash: BlockHash): bool = all {
el_validated.contains(parent_hash), // precondition: parent must be validated (can't validate orphan)
not(el_validated.contains(block_hash)), // precondition: block was not already validated
el_validated' = el_validated.union(Set(block_hash)), // block becomes validated
last_response' = Valid,
cl_head' = cl_head, // no change to heads or building state
el_head' = el_head,
el_building' = el_building,
el_built' = el_built,
}
/// NewPayload - invalid block
///
/// Block fails validation (bad state root, invalid transactions, etc.)
action newPayload_invalid(block_hash: BlockHash): bool = all {
last_response' = Invalid, // block is NOT added to validated set
cl_head' = cl_head, // no state changes
el_head' = el_head,
el_validated' = el_validated,
el_building' = el_building,
el_built' = el_built,
}
// ==========================================================================
// STATE MACHINE
// ==========================================================================
/// All possible block hashes we model (keep small for tractability)
pure val ALL_BLOCKS: Set[BlockHash] = Set(GENESIS, BLOCK1, BLOCK2, BLOCK3)
/// All possible payload IDs
pure val ALL_PAYLOAD_IDS: Set[PayloadId] = Set(PID1, PID2)
/// Non-deterministic step: any action can happen
action step = any {
nondet block_hash = ALL_BLOCKS.oneOf()
forkchoiceUpdated_setHead(block_hash),
nondet block_hash = ALL_BLOCKS.oneOf()
forkchoiceUpdated_unknown(block_hash),
nondet parent = ALL_BLOCKS.oneOf()
nondet pid = ALL_PAYLOAD_IDS.oneOf()
forkchoiceUpdated_buildBlock(parent, pid),
nondet pid = ALL_PAYLOAD_IDS.oneOf()
nondet new_hash = ALL_BLOCKS.oneOf()
el_finishBuilding(pid, new_hash),
nondet pid = ALL_PAYLOAD_IDS.oneOf()
getPayload(pid),
nondet block_hash = ALL_BLOCKS.oneOf()
nondet parent = ALL_BLOCKS.oneOf()
newPayload_valid(block_hash, parent),
nondet block_hash = ALL_BLOCKS.oneOf()
newPayload_invalid(block_hash),
}
// ==========================================================================
// SAFETY PROPERTIES (invariants)
// ==========================================================================
/// EL's canonical head is always a validated block.
///
/// If this fails, EL could report an invalid block as canonical.
val headIsValidated: bool =
el_validated.contains(el_head)
/// After a VALID response, CL and EL agree on head.
val headsConsistent: bool =
last_response == Valid implies cl_head == el_head
/// Genesis is always in the validated set.
///
/// We should never "un-validate" our starting point.
val genesisAlwaysValid: bool =
el_validated.contains(GENESIS)
/// Built blocks are always validated.
///
/// If EL built it, it must be in the validated pool.
val builtBlocksAreValidated: bool =
el_built.keys().forall(pid =>
el_validated.contains(el_built.get(pid))
)
/// Combined safety invariant
val safety: bool = and {
headIsValidated,
headsConsistent,
genesisAlwaysValid,
builtBlocksAreValidated,
}
// ==========================================================================
// TESTS
// ==========================================================================
/// Test: basic forkchoice flow
run basicForkchoiceTest = {
init
.then(newPayload_valid(BLOCK1, GENESIS))
.then(forkchoiceUpdated_setHead(BLOCK1))
.then(all {
assert(cl_head == BLOCK1),
assert(el_head == BLOCK1),
assert(last_response == Valid),
cl_head' = cl_head,
el_head' = el_head,
el_validated' = el_validated,
el_building' = el_building,
el_built' = el_built,
last_response' = last_response,
})
}
/// Test: block building flow
run blockBuildingTest = {
init
.then(forkchoiceUpdated_buildBlock(GENESIS, PID1))
.then(el_finishBuilding(PID1, BLOCK1))
.then(getPayload(PID1))
.then(all {
assert(el_built.get(PID1) == BLOCK1),
assert(el_validated.contains(BLOCK1)),
cl_head' = cl_head,
el_head' = el_head,
el_validated' = el_validated,
el_building' = el_building,
el_built' = el_built,
last_response' = last_response,
})
}
/// Test: unknown block returns SYNCING
run unknownBlockTest = {
init
.then(forkchoiceUpdated_unknown(BLOCK1)) // BLOCK1 not yet validated
.then(all {
assert(last_response == Syncing),
assert(cl_head == GENESIS), // head unchanged
cl_head' = cl_head,
el_head' = el_head,
el_validated' = el_validated,
el_building' = el_building,
el_built' = el_built,
last_response' = last_response,
})
}
}