Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
execution_discard.test.cpp
Go to the documentation of this file.
1#include <gmock/gmock.h>
2#include <gtest/gtest.h>
3
4#include <cstdint>
5
13
14namespace bb::avm2::constraining {
15namespace {
16
17using tracegen::TestTraceContainer;
19using C = Column;
20using execution_discard = bb::avm2::discard<FF>;
21
22TEST(ExecutionDiscardConstrainingTest, EmptyRow)
23{
24 check_relation<execution_discard>(testing::empty_trace());
25}
26
27TEST(ExecutionDiscardConstrainingTest, DiscardIffDyingContext)
28{
29 // Test that discard=1 <=> dying_context_id!=0
30 TestTraceContainer trace({
31 { { C::precomputed_first_row, 1 } },
32 // discard=0 => dying_context_id=0
33 { { C::execution_sel, 1 },
34 { C::execution_discard, 0 },
35 { C::execution_dying_context_id, 0 },
36 { C::execution_dying_context_id_inv, 0 } },
37 // discard=1 => dying_context_id!=0
38 { { C::execution_sel, 1 },
39 { C::execution_discard, 1 },
40 { C::execution_dying_context_id, 42 },
41 { C::execution_dying_context_id_inv, FF(42).invert() } },
42 { { C::execution_sel, 1 } },
43 { { C::execution_sel, 0 } },
44 });
45
46 // Only check subrelations 3 and 4 (discard/dying_context_id relationship)
47 check_relation<execution_discard>(
48 trace, execution_discard::SR_DISCARD_IFF_DYING_CONTEXT, execution_discard::SR_DISCARD_IF_FAILURE);
49
50 // Negative test: discard=1 but dying_context_id=0
51 trace.set(C::execution_dying_context_id, 2, 0);
52 trace.set(C::execution_dying_context_id_inv, 2, 0);
53 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_DISCARD_IFF_DYING_CONTEXT),
54 "DISCARD_IFF_DYING_CONTEXT");
55
56 // Reset before next test
57 trace.set(C::execution_dying_context_id, 1, 0);
58 trace.set(C::execution_dying_context_id_inv, 1, 0);
59 trace.set(C::execution_dying_context_id, 2, 42);
60 trace.set(C::execution_dying_context_id_inv, 2, FF(42).invert());
61
62 // Negative test: discard=0 but dying_context_id!=0
63 trace.set(C::execution_dying_context_id, 1, 42);
64 trace.set(C::execution_dying_context_id_inv, 1, FF(42).invert());
65 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_DISCARD_IFF_DYING_CONTEXT),
66 "DISCARD_IFF_DYING_CONTEXT");
67}
68
69TEST(ExecutionDiscardConstrainingTest, DiscardFailureMustDiscard)
70{
71 // Test that sel_failure=1 => discard=1
72 TestTraceContainer trace({
73 { { C::precomputed_first_row, 1 } },
74 // Failure with discard
75 { { C::execution_sel, 1 },
76 { C::execution_sel_failure, 1 },
77 { C::execution_discard, 1 },
78 { C::execution_dying_context_id, 42 },
79 { C::execution_dying_context_id_inv, FF(42).invert() } },
80 // No failure, no discard
81 { { C::execution_sel, 1 },
82 { C::execution_sel_failure, 0 },
83 { C::execution_discard, 0 },
84 { C::execution_dying_context_id, 0 },
85 { C::execution_dying_context_id_inv, 0 } },
86 // Discard doesn't imply failure
87 { { C::execution_sel, 1 },
88 { C::execution_sel_failure, 0 },
89 { C::execution_discard, 1 },
90 { C::execution_dying_context_id, 0 } },
91 { { C::execution_sel, 1 } },
92 { { C::execution_sel, 0 } },
93 });
94
95 // Only check subrelation 5 (failure must discard)
96 check_relation<execution_discard>(trace, execution_discard::SR_DISCARD_IF_FAILURE);
97
98 // Negative test: failure but no discard
99 trace.set(C::execution_discard, 1, 0);
100 trace.set(C::execution_dying_context_id, 1, 0);
101 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_DISCARD_IF_FAILURE),
102 "DISCARD_IF_FAILURE");
103}
104
105TEST(ExecutionDiscardConstrainingTest, DiscardIsDyingContextCheck)
106{
107 // Test the is_dying_context calculation
108 TestTraceContainer trace({
109 { { C::precomputed_first_row, 1 } },
110 // context_id=5, dying_context_id=5 => is_dying_context=1
111 { { C::execution_sel, 1 },
112 { C::execution_context_id, 5 },
113 { C::execution_discard, 1 },
114 { C::execution_dying_context_id, 5 },
115 { C::execution_is_dying_context, 1 },
116 { C::execution_dying_context_diff_inv, 0 } },
117 // context_id=3, dying_context_id=5 => is_dying_context=0, diff_inv=(3-5)^(-1)=(-2)^(-1)
118 { { C::execution_sel, 1 },
119 { C::execution_context_id, 3 },
120 { C::execution_discard, 1 },
121 { C::execution_dying_context_id, 5 },
122 { C::execution_dying_context_id_inv, FF(5).invert() },
123 { C::execution_is_dying_context, 0 },
124 { C::execution_dying_context_diff_inv, FF(3 - 5).invert() } },
125 // discard=0 case (is_dying_context should be 0)
126 { { C::execution_sel, 1 },
127 { C::execution_context_id, 7 },
128 { C::execution_discard, 0 },
129 { C::execution_dying_context_id, 0 },
130 { C::execution_is_dying_context, 0 },
131 { C::execution_dying_context_diff_inv, FF(7 - 0).invert() } },
132 { { C::execution_sel, 0 } },
133 });
134
135 check_relation<execution_discard>(trace, execution_discard::SR_IS_DYING_CONTEXT_CHECK);
136
137 // Negative test: wrong is_dying_context when equal
138 trace.set(C::execution_is_dying_context, 1, 0);
139 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_IS_DYING_CONTEXT_CHECK),
140 "IS_DYING_CONTEXT_CHECK");
141
142 // Negative test: wrong is_dying_context when not equal
143 trace.set(C::execution_is_dying_context, 1, 1); // Reset
144 trace.set(C::execution_is_dying_context, 2, 1);
145 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_IS_DYING_CONTEXT_CHECK),
146 "IS_DYING_CONTEXT_CHECK");
147}
148
149TEST(ExecutionDiscardConstrainingTest, DiscardPropagationOfZeroDiscard)
150{
151 TestTraceContainer trace({
152 { { C::precomputed_first_row, 1 } },
153 {
154 { C::execution_sel, 1 },
155 { C::execution_discard, 0 },
156 { C::execution_dying_context_id, 0 },
157 { C::execution_sel_exit_call, 0 },
158 { C::execution_has_parent_ctx, 1 },
159 { C::execution_sel_failure, 0 },
160 { C::execution_is_dying_context, 0 },
161 { C::execution_sel_enter_call, 0 },
162 { C::execution_enqueued_call_end, 0 },
163 { C::execution_resolves_dying_context, 0 },
164 { C::execution_nested_call_from_undiscarded_context, 0 },
165 },
166 // Propagates to next row
167 { { C::execution_sel, 1 },
168 { C::execution_discard, 0 },
169 { C::execution_dying_context_id, 0 },
170 { C::execution_dying_context_id_inv, 0 },
171 { C::execution_enqueued_call_end, 0 },
172 { C::execution_resolves_dying_context, 0 },
173 { C::execution_nested_call_from_undiscarded_context, 0 } },
174 // Last row gets propagated discard values. Propagation doesn't apply to next row because last=1.
175 { { C::execution_sel, 1 }, { C::execution_discard, 0 }, { C::execution_dying_context_id, 0 } },
176 { { C::execution_sel, 0 } },
177 });
178
179 check_relation<execution_discard>(
180 trace, execution_discard::SR_DISCARD_PROPAGATION, execution_discard::SR_DYING_CONTEXT_PROPAGATION);
181
182 // Negative test: doesn't propagate but it should.
183 trace.set(C::execution_discard, 2, 42);
184 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_DISCARD_PROPAGATION),
185 "DISCARD_PROPAGATION");
186}
187
188TEST(ExecutionDiscardConstrainingTest, DiscardPropagationOfNonzeroDiscard)
189{
190 TestTraceContainer trace({
191 { { C::precomputed_first_row, 1 } },
192 // Normal propagation case
193 { { C::execution_sel, 1 },
194 { C::execution_discard, 1 },
195 { C::execution_dying_context_id, 42 },
196 { C::execution_sel_exit_call, 0 },
197 { C::execution_has_parent_ctx, 1 },
198 { C::execution_sel_failure, 0 },
199 { C::execution_is_dying_context, 0 },
200 { C::execution_sel_enter_call, 0 },
201 { C::execution_enqueued_call_end, 0 },
202 { C::execution_resolves_dying_context, 0 },
203 { C::execution_nested_call_from_undiscarded_context, 0 } },
204 // Propagates to next row
205 { { C::execution_sel, 1 },
206 { C::execution_discard, 1 },
207 { C::execution_dying_context_id, 42 },
208 { C::execution_enqueued_call_end, 0 },
209 { C::execution_resolves_dying_context, 0 },
210 { C::execution_nested_call_from_undiscarded_context, 0 } },
211 // Last row gets propagated discard values. Propagation doesn't apply to next row because last=1.
212 { { C::execution_sel, 1 }, { C::execution_discard, 1 }, { C::execution_dying_context_id, 42 } },
213 { { C::execution_sel, 0 } },
214 });
215
216 check_relation<execution_discard>(
217 trace, execution_discard::SR_DISCARD_PROPAGATION, execution_discard::SR_DYING_CONTEXT_PROPAGATION);
218
219 // Negative test: doesn't propagate but it should.
220 trace.set(C::execution_discard, 2, 0);
221 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_DISCARD_PROPAGATION),
222 "DISCARD_PROPAGATION");
223}
224
225TEST(ExecutionDiscardConstrainingTest, DiscardPropagationLiftedEndOfEnqueuedCall)
226{
227 // Test propagation lifted at end of enqueued call (exit_call && !has_parent)
228 TestTraceContainer trace({
229 { { C::precomputed_first_row, 1 } },
230 // Exiting top-level call - propagation lifted
231 { { C::execution_sel, 1 },
232 { C::execution_discard, 1 },
233 { C::execution_dying_context_id, 42 },
234 { C::execution_sel_exit_call, 1 },
235 { C::execution_has_parent_ctx, 0 },
236 { C::execution_enqueued_call_end, 1 },
237 { C::execution_resolves_dying_context, 0 },
238 { C::execution_nested_call_from_undiscarded_context, 0 } },
239 // Next row can have different discard values
240 { { C::execution_sel, 1 }, { C::execution_discard, 0 }, { C::execution_dying_context_id, 0 } },
241 { { C::execution_sel, 1 } },
242 { { C::execution_sel, 0 } },
243 });
244
245 check_relation<execution_discard>(
246 trace, execution_discard::SR_DISCARD_PROPAGATION, execution_discard::SR_DYING_CONTEXT_PROPAGATION);
247}
248
249TEST(ExecutionDiscardConstrainingTest, DiscardPropagationLiftedResolvesDyingContext)
250{
251 // Test propagation lifted when resolving dying context (sel_failure && is_dying_context)
252 TestTraceContainer trace({
253 { { C::precomputed_first_row, 1 } },
254 // Failure in dying context - propagation lifted
255 { { C::execution_sel, 1 },
256 { C::execution_context_id, 42 },
257 { C::execution_discard, 1 },
258 { C::execution_dying_context_id, 42 },
259 { C::execution_sel_failure, 1 },
260 { C::execution_is_dying_context, 1 },
261 { C::execution_dying_context_diff_inv, 0 },
262 { C::execution_enqueued_call_end, 0 },
263 { C::execution_resolves_dying_context, 1 },
264 { C::execution_nested_call_from_undiscarded_context, 0 } },
265 // Next row can have different discard values
266 { { C::execution_sel, 1 },
267 { C::execution_discard, 0 },
268 { C::execution_dying_context_id, 0 },
269 { C::execution_enqueued_call_end, 0 },
270 { C::execution_resolves_dying_context, 0 },
271 { C::execution_nested_call_from_undiscarded_context, 0 } },
272 { { C::execution_sel, 1 } },
273 { { C::execution_sel, 0 } },
274 });
275
276 check_relation<execution_discard>(
277 trace, execution_discard::SR_DISCARD_PROPAGATION, execution_discard::SR_DYING_CONTEXT_PROPAGATION);
278}
279
280TEST(ExecutionDiscardConstrainingTest, DiscardPropagationLiftedNestedCallFromUndiscarded)
281{
282 // Test propagation lifted when making nested call from undiscarded context
283 TestTraceContainer trace({
284 { { C::precomputed_first_row, 1 } },
285 // Making a call from undiscarded context - propagation lifted
286 { { C::execution_sel, 1 },
287 { C::execution_discard, 0 },
288 { C::execution_dying_context_id, 0 },
289 { C::execution_sel_enter_call, 1 },
290 { C::execution_enqueued_call_end, 0 },
291 { C::execution_resolves_dying_context, 0 },
292 { C::execution_nested_call_from_undiscarded_context, 1 } },
293 // Next row can raise discard (nested context will error)
294 { { C::execution_sel, 1 }, { C::execution_discard, 1 }, { C::execution_dying_context_id, 99 } },
295 // Last row keeps the values (propagation doesn't apply because last=1)
296 { { C::execution_sel, 1 }, { C::execution_discard, 1 }, { C::execution_dying_context_id, 99 } },
297 { { C::execution_sel, 0 } },
298 });
299
300 // This should pass because sel_enter_call=1 lifts the propagation constraint
301 check_relation<execution_discard>(
302 trace, execution_discard::SR_DISCARD_PROPAGATION, execution_discard::SR_DYING_CONTEXT_PROPAGATION);
303}
304
305TEST(ExecutionDiscardConstrainingTest, DiscardDyingContextMustError)
306{
307 // Test that dying context must exit with failure
308 TestTraceContainer trace({
309 { { C::precomputed_first_row, 1 } },
310 // Dying context exits with error - OK
311 { { C::execution_sel, 1 },
312 { C::execution_context_id, 42 },
313 { C::execution_discard, 1 },
314 { C::execution_dying_context_id, 42 },
315 { C::execution_is_dying_context, 1 },
316 { C::execution_sel_exit_call, 1 },
317 { C::execution_sel_error, 1 },
318 { C::execution_sel_execute_revert, 0 },
319 { C::execution_sel_failure, 1 },
320 { C::execution_dying_context_diff_inv, 0 } },
321 { { C::execution_sel, 1 } },
322 { { C::execution_sel, 0 } },
323 });
324
325 check_relation<execution_discard>(trace, execution_discard::SR_DYING_CONTEXT_MUST_FAIL);
326
327 // Negative test: dying context exits without error
328 trace.set(C::execution_sel_failure, 1, 0);
329 trace.set(C::execution_sel_error, 1, 0);
330 trace.set(C::execution_sel_execute_revert, 1, 0);
331 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace, execution_discard::SR_DYING_CONTEXT_MUST_FAIL),
332 "DYING_CONTEXT_MUST_FAIL");
333}
334
335TEST(ExecutionDiscardConstrainingTest, DiscardComplexScenario)
336{
337 // Complex scenario: nested calls with errors
338 TestTraceContainer trace({
339 { { C::precomputed_first_row, 1 } },
340 // Row 1: Parent context, no discard
341 { { C::execution_sel, 1 },
342 { C::execution_context_id, 1 },
343 { C::execution_discard, 0 },
344 { C::execution_dying_context_id, 0 },
345 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
346 { C::execution_is_dying_context, 0 },
347 { C::execution_enqueued_call_end, 0 },
348 { C::execution_resolves_dying_context, 0 },
349 { C::execution_nested_call_from_undiscarded_context, 0 } },
350 // Row 2: Call to nested context (that will eventually error)
351 { { C::execution_sel, 1 },
352 { C::execution_context_id, 1 },
353 { C::execution_discard, 0 },
354 { C::execution_dying_context_id, 0 },
355 { C::execution_dying_context_id_inv, 0 },
356 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
357 { C::execution_sel_enter_call, 1 },
358 { C::execution_enqueued_call_end, 0 },
359 { C::execution_resolves_dying_context, 0 },
360 { C::execution_nested_call_from_undiscarded_context, 1 } },
361 // Row 3: Nested context, discard raised because this context will error
362 { { C::execution_sel, 1 },
363 { C::execution_context_id, 2 },
364 { C::execution_discard, 1 },
365 { C::execution_dying_context_id, 2 },
366 { C::execution_is_dying_context, 1 },
367 { C::execution_dying_context_diff_inv, 0 },
368 { C::execution_enqueued_call_end, 0 },
369 { C::execution_resolves_dying_context, 0 },
370 { C::execution_nested_call_from_undiscarded_context, 0 } },
371 // Row 4: Nested context errors
372 { { C::execution_sel, 1 },
373 { C::execution_context_id, 2 },
374 { C::execution_discard, 1 },
375 { C::execution_dying_context_id, 2 },
376 { C::execution_is_dying_context, 1 },
377 { C::execution_sel_exit_call, 1 },
378 { C::execution_sel_error, 1 },
379 { C::execution_sel_execute_revert, 0 },
380 { C::execution_sel_failure, 1 },
381 { C::execution_dying_context_diff_inv, 0 },
382 { C::execution_has_parent_ctx, 1 },
383 { C::execution_enqueued_call_end, 0 },
384 { C::execution_resolves_dying_context, 1 },
385 { C::execution_nested_call_from_undiscarded_context, 0 } },
386 // Row 5: Back to parent, discard cleared
387 { { C::execution_sel, 1 },
388 { C::execution_context_id, 1 },
389 { C::execution_discard, 0 },
390 { C::execution_dying_context_id, 0 },
391 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
392 { C::execution_is_dying_context, 0 },
393 { C::execution_enqueued_call_end, 0 },
394 { C::execution_resolves_dying_context, 0 },
395 { C::execution_nested_call_from_undiscarded_context, 0 } },
396 { { C::execution_sel, 0 } },
397 });
398
399 // Only check the most important relations for this scenario
400 check_relation<execution_discard>(trace,
401 execution_discard::SR_IS_DYING_CONTEXT_CHECK,
402 execution_discard::SR_DISCARD_PROPAGATION,
403 execution_discard::SR_DYING_CONTEXT_PROPAGATION,
404 execution_discard::SR_DYING_CONTEXT_MUST_FAIL);
405}
406
407TEST(ExecutionDiscardConstrainingTest, DiscardWithLastRow)
408{
409 // Test discard behavior with last row
410 TestTraceContainer trace(
411 { { { C::precomputed_first_row, 1 } },
412 { { C::execution_sel, 1 },
413 { C::execution_discard, 1 },
414 { C::execution_dying_context_id, 42 },
415 { C::execution_enqueued_call_end, 0 },
416 { C::execution_resolves_dying_context, 0 },
417 { C::execution_nested_call_from_undiscarded_context, 0 } },
418 // Last row also has discard values (propagation doesn't apply because last=1)
419 { { C::execution_sel, 1 }, { C::execution_discard, 1 }, { C::execution_dying_context_id, 42 } },
420 { { C::execution_sel, 0 } } });
421
422 check_relation<execution_discard>(
423 trace, execution_discard::SR_DISCARD_PROPAGATION, execution_discard::SR_DYING_CONTEXT_PROPAGATION);
424}
425
426// ====== EXPLOIT TESTS - These test vulnerabilities found in early versions ======
427
428TEST(ExecutionDiscardConstrainingTest, ExploitRaiseDiscardWithWrongDyingContext)
429{
430 // EXPLOIT 1: A calls B calls C. C fails.
431 // Attacker raises discard when entering B and sets dying context to C.
432 // Then C clears the flag when it fails.
433 // Result on attack success: B's operations are discarded even though B didn't fail.
434 TestTraceContainer trace({
435 { { C::precomputed_first_row, 1 } },
436 // Row 1: Context A (id=1), no discard initially
437 { { C::execution_sel, 1 },
438 { C::execution_context_id, 1 },
439 { C::execution_discard, 0 },
440 { C::execution_dying_context_id, 0 },
441 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
442 { C::execution_is_dying_context, 0 },
443 { C::execution_enqueued_call_end, 0 },
444 { C::execution_resolves_dying_context, 0 },
445 { C::execution_nested_call_from_undiscarded_context, 0 } },
446 // Row 2: A calls B - ATTACK: raise discard and set dying_context to C (id=3)
447 { { C::execution_sel, 1 },
448 { C::execution_context_id, 1 },
449 { C::execution_discard, 0 },
450 { C::execution_dying_context_id, 0 },
451 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
452 { C::execution_sel_enter_call, 1 },
453 { C::execution_enqueued_call_end, 0 },
454 { C::execution_resolves_dying_context, 0 },
455 { C::execution_nested_call_from_undiscarded_context, 1 } },
456 // Row 3: Entering B (id=2) - ATTACK: discard raised to 1, dying_context set to 3 (C)
457 { { C::execution_sel, 1 },
458 { C::execution_context_id, 2 },
459 { C::execution_discard, 1 },
460 { C::execution_dying_context_id, 3 },
461 { C::execution_dying_context_id_inv, FF(3).invert() },
462 { C::execution_dying_context_diff_inv, FF(2 - 3).invert() },
463 { C::execution_is_dying_context, 0 },
464 { C::execution_enqueued_call_end, 0 },
465 { C::execution_resolves_dying_context, 0 },
466 { C::execution_nested_call_from_undiscarded_context, 0 } },
467 // Row 4: B calls C
468 { { C::execution_sel, 1 },
469 { C::execution_context_id, 2 },
470 { C::execution_discard, 1 },
471 { C::execution_dying_context_id, 3 },
472 { C::execution_dying_context_id_inv, FF(3).invert() },
473 { C::execution_dying_context_diff_inv, FF(2 - 3).invert() },
474 { C::execution_is_dying_context, 0 },
475 { C::execution_sel_enter_call, 1 },
476 { C::execution_enqueued_call_end, 0 },
477 { C::execution_resolves_dying_context, 0 },
478 { C::execution_nested_call_from_undiscarded_context, 0 } },
479 // Row 5: C (id=3) executes and fails - this is the dying context
480 { { C::execution_sel, 1 },
481 { C::execution_context_id, 3 },
482 { C::execution_discard, 1 },
483 { C::execution_dying_context_id, 3 },
484 { C::execution_dying_context_id_inv, FF(3).invert() },
485 { C::execution_dying_context_diff_inv, 0 },
486 { C::execution_is_dying_context, 1 },
487 { C::execution_sel_exit_call, 1 },
488 { C::execution_sel_error, 1 },
489 { C::execution_sel_failure, 1 },
490 { C::execution_has_parent_ctx, 1 },
491 { C::execution_enqueued_call_end, 0 },
492 { C::execution_resolves_dying_context, 1 },
493 { C::execution_nested_call_from_undiscarded_context, 0 } },
494 // Row 6: Back to B - discard cleared because dying context resolved
495 { { C::execution_sel, 1 },
496 { C::execution_context_id, 2 },
497 { C::execution_discard, 0 },
498 { C::execution_dying_context_id, 0 },
499 { C::execution_dying_context_diff_inv, FF(2 - 0).invert() },
500 { C::execution_is_dying_context, 0 },
501 { C::execution_enqueued_call_end, 0 },
502 { C::execution_resolves_dying_context, 0 },
503 { C::execution_nested_call_from_undiscarded_context, 0 } },
504 // Row 7: B exits successfully (no failure)
505 { { C::execution_sel, 1 },
506 { C::execution_context_id, 2 },
507 { C::execution_discard, 0 },
508 { C::execution_dying_context_id, 0 },
509 { C::execution_dying_context_diff_inv, FF(2 - 0).invert() },
510 { C::execution_sel_exit_call, 1 },
511 { C::execution_sel_error, 0 },
512 { C::execution_sel_failure, 0 },
513 { C::execution_has_parent_ctx, 1 },
514 { C::execution_enqueued_call_end, 0 },
515 { C::execution_resolves_dying_context, 0 },
516 { C::execution_nested_call_from_undiscarded_context, 0 } },
517 { { C::execution_sel, 1 } },
518 { { C::execution_sel, 0 } },
519 });
520
521 // If the exploit works, this check will pass.
522 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace), "ENTER_CALL_DISCARD_MUST_BE_DYING_CONTEXT");
523}
524
525TEST(ExecutionDiscardConstrainingTest, ExploitAvoidDiscardByDelayingRaise)
526{
527 // EXPLOIT 2: A calls B calls C. B and C both fail.
528 // Attacker doesn't raise discard until it enters C, but sets the dying context to B.
529 // Then discard will remain 1 until it is cleared at the end of B.
530 // Result on attack success: B's rows before calling C are not discarded despite B's eventual failure.
531 TestTraceContainer trace({
532 { { C::precomputed_first_row, 1 } },
533 // Row 1: Context A (id=1), no discard
534 { { C::execution_sel, 1 },
535 { C::execution_context_id, 1 },
536 { C::execution_discard, 0 },
537 { C::execution_dying_context_id, 0 },
538 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
539 { C::execution_enqueued_call_end, 0 },
540 { C::execution_resolves_dying_context, 0 },
541 { C::execution_nested_call_from_undiscarded_context, 0 } },
542 // Row 2: A calls B
543 { { C::execution_sel, 1 },
544 { C::execution_context_id, 1 },
545 { C::execution_discard, 0 },
546 { C::execution_dying_context_id, 0 },
547 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
548 { C::execution_sel_enter_call, 1 },
549 { C::execution_enqueued_call_end, 0 },
550 { C::execution_resolves_dying_context, 0 },
551 { C::execution_nested_call_from_undiscarded_context, 1 } },
552 // Row 3: B (id=2) executes - ATTACK: don't raise discard yet
553 { { C::execution_sel, 1 },
554 { C::execution_context_id, 2 },
555 { C::execution_discard, 0 },
556 { C::execution_dying_context_id, 0 },
557 { C::execution_dying_context_diff_inv, FF(2 - 0).invert() },
558 { C::execution_is_dying_context, 0 },
559 { C::execution_enqueued_call_end, 0 },
560 { C::execution_resolves_dying_context, 0 },
561 { C::execution_nested_call_from_undiscarded_context, 0 } },
562 // Row 4: B calls C
563 { { C::execution_sel, 1 },
564 { C::execution_context_id, 2 },
565 { C::execution_discard, 0 },
566 { C::execution_dying_context_id, 0 },
567 { C::execution_dying_context_diff_inv, FF(2 - 0).invert() },
568 { C::execution_sel_enter_call, 1 },
569 { C::execution_enqueued_call_end, 0 },
570 { C::execution_resolves_dying_context, 0 },
571 { C::execution_nested_call_from_undiscarded_context, 1 } },
572 // Row 5: Entering C (id=3) - ATTACK: NOW raise discard but set dying_context to B (id=2)
573 { { C::execution_sel, 1 },
574 { C::execution_context_id, 3 },
575 { C::execution_discard, 1 },
576 { C::execution_dying_context_id, 2 },
577 { C::execution_dying_context_id_inv, FF(2).invert() },
578 { C::execution_dying_context_diff_inv, FF(3 - 2).invert() },
579 { C::execution_is_dying_context, 0 },
580 { C::execution_enqueued_call_end, 0 },
581 { C::execution_resolves_dying_context, 0 },
582 { C::execution_nested_call_from_undiscarded_context, 0 } },
583 // Row 6: C fails, but it's not the dying context so discard propagates
584 { { C::execution_sel, 1 },
585 { C::execution_context_id, 3 },
586 { C::execution_discard, 1 },
587 { C::execution_dying_context_id, 2 },
588 { C::execution_dying_context_id_inv, FF(2).invert() },
589 { C::execution_dying_context_diff_inv, FF(3 - 2).invert() },
590 { C::execution_is_dying_context, 0 },
591 { C::execution_sel_exit_call, 1 },
592 { C::execution_sel_error, 1 },
593 { C::execution_sel_failure, 1 },
594 { C::execution_has_parent_ctx, 1 },
595 { C::execution_enqueued_call_end, 0 },
596 { C::execution_resolves_dying_context, 0 },
597 { C::execution_nested_call_from_undiscarded_context, 0 } },
598 // Row 7: Back to B, discard still 1
599 { { C::execution_sel, 1 },
600 { C::execution_context_id, 2 },
601 { C::execution_discard, 1 },
602 { C::execution_dying_context_id, 2 },
603 { C::execution_dying_context_id_inv, FF(2).invert() },
604 { C::execution_dying_context_diff_inv, 0 },
605 { C::execution_is_dying_context, 1 },
606 { C::execution_enqueued_call_end, 0 },
607 { C::execution_resolves_dying_context, 0 },
608 { C::execution_nested_call_from_undiscarded_context, 0 } },
609 // Row 8: B fails and is the dying context, so discard gets cleared
610 { { C::execution_sel, 1 },
611 { C::execution_context_id, 2 },
612 { C::execution_discard, 1 },
613 { C::execution_dying_context_id, 2 },
614 { C::execution_dying_context_id_inv, FF(2).invert() },
615 { C::execution_dying_context_diff_inv, 0 },
616 { C::execution_is_dying_context, 1 },
617 { C::execution_sel_exit_call, 1 },
618 { C::execution_sel_error, 1 },
619 { C::execution_sel_failure, 1 },
620 { C::execution_has_parent_ctx, 1 },
621 { C::execution_enqueued_call_end, 0 },
622 { C::execution_resolves_dying_context, 1 },
623 { C::execution_nested_call_from_undiscarded_context, 0 } },
624 { { C::execution_sel, 1 } },
625 { { C::execution_sel, 0 } },
626 });
627
628 // If the exploit works, this check will pass.
629 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace), "ENTER_CALL_DISCARD_MUST_BE_DYING_CONTEXT");
630}
631
632TEST(ExecutionDiscardConstrainingTest, ExploitChangesDyingContextAfterResolution)
633{
634 // EXPLOIT 3: A calls B calls C. B and C both fail.
635 // Attacker sets dying context to C initially. When C dies, attacker changes dying context to B
636 // instead of clearing discard, allowing them to avoid discarding B's early operations.
637 // Result on attack success: B's rows before calling C are not discarded despite B's eventual failure.
638 TestTraceContainer trace({
639 { { C::precomputed_first_row, 1 } },
640 // Row 1: Context A calls B
641 { { C::execution_sel, 1 },
642 { C::execution_context_id, 1 },
643 { C::execution_discard, 0 },
644 { C::execution_dying_context_id, 0 },
645 { C::execution_dying_context_diff_inv, FF(1 - 0).invert() },
646 { C::execution_sel_enter_call, 1 },
647 { C::execution_enqueued_call_end, 0 },
648 { C::execution_resolves_dying_context, 0 },
649 { C::execution_nested_call_from_undiscarded_context, 1 } },
650 // Row 2: B (id=2) executes - not discarded yet
651 { { C::execution_sel, 1 },
652 { C::execution_context_id, 2 },
653 { C::execution_discard, 0 },
654 { C::execution_dying_context_id, 0 },
655 { C::execution_dying_context_diff_inv, FF(2 - 0).invert() },
656 { C::execution_enqueued_call_end, 0 },
657 { C::execution_resolves_dying_context, 0 },
658 { C::execution_nested_call_from_undiscarded_context, 0 } },
659 // Row 3: B calls C
660 { { C::execution_sel, 1 },
661 { C::execution_context_id, 2 },
662 { C::execution_discard, 0 },
663 { C::execution_dying_context_id, 0 },
664 { C::execution_dying_context_diff_inv, FF(2 - 0).invert() },
665 { C::execution_sel_enter_call, 1 },
666 { C::execution_enqueued_call_end, 0 },
667 { C::execution_resolves_dying_context, 0 },
668 { C::execution_nested_call_from_undiscarded_context, 1 } },
669 // Row 4: Entering C (id=3) - raise discard, set dying_context to C
670 { { C::execution_sel, 1 },
671 { C::execution_context_id, 3 },
672 { C::execution_discard, 1 },
673 { C::execution_dying_context_id, 3 },
674 { C::execution_dying_context_id_inv, FF(3).invert() },
675 { C::execution_dying_context_diff_inv, 0 },
676 { C::execution_is_dying_context, 1 },
677 { C::execution_enqueued_call_end, 0 },
678 { C::execution_resolves_dying_context, 0 },
679 { C::execution_nested_call_from_undiscarded_context, 0 } },
680 // Row 5: C fails (dying context resolves, propagation lifted)
681 { { C::execution_sel, 1 },
682 { C::execution_context_id, 3 },
683 { C::execution_discard, 1 },
684 { C::execution_dying_context_id, 3 },
685 { C::execution_dying_context_id_inv, FF(3).invert() },
686 { C::execution_dying_context_diff_inv, 0 },
687 { C::execution_is_dying_context, 1 },
688 { C::execution_sel_exit_call, 1 },
689 { C::execution_sel_error, 1 },
690 { C::execution_sel_failure, 1 },
691 { C::execution_has_parent_ctx, 1 },
692 { C::execution_enqueued_call_end, 0 },
693 { C::execution_resolves_dying_context, 1 },
694 { C::execution_nested_call_from_undiscarded_context, 0 } },
695 // Row 6: Back to B - ATTACK: keep discard=1 but change dying context to B
696 { { C::execution_sel, 1 },
697 { C::execution_context_id, 2 },
698 { C::execution_discard, 1 },
699 { C::execution_dying_context_id, 2 },
700 { C::execution_dying_context_id_inv, FF(2).invert() },
701 { C::execution_dying_context_diff_inv, 0 },
702 { C::execution_is_dying_context, 1 },
703 { C::execution_enqueued_call_end, 0 },
704 { C::execution_resolves_dying_context, 0 },
705 { C::execution_nested_call_from_undiscarded_context, 0 } },
706 // Row 7: B fails and resolves as dying context, clearing discard again.
707 { { C::execution_sel, 1 },
708 { C::execution_context_id, 2 },
709 { C::execution_discard, 1 },
710 { C::execution_dying_context_id, 2 },
711 { C::execution_dying_context_id_inv, FF(2).invert() },
712 { C::execution_dying_context_diff_inv, 0 },
713 { C::execution_is_dying_context, 1 },
714 { C::execution_sel_exit_call, 1 },
715 { C::execution_sel_error, 1 },
716 { C::execution_sel_failure, 1 },
717 { C::execution_has_parent_ctx, 1 },
718 { C::execution_enqueued_call_end, 0 },
719 { C::execution_resolves_dying_context, 1 },
720 { C::execution_nested_call_from_undiscarded_context, 0 } },
721 { { C::execution_sel, 1 } },
722 { { C::execution_sel, 0 } },
723 });
724
725 // If the exploit works, this check will pass.
726 EXPECT_THROW_WITH_MESSAGE(check_relation<execution_discard>(trace), "DYING_CONTEXT_WITH_PARENT_MUST_CLEAR_DISCARD");
727}
728
729} // namespace
730} // namespace bb::avm2::constraining
void set(Column col, uint32_t row, const FF &value)
TestTraceContainer trace
#define EXPECT_THROW_WITH_MESSAGE(code, expectedMessage)
Definition macros.hpp:7
TEST(TxExecutionConstrainingTest, WriteTreeValue)
Definition tx.test.cpp:441
TestTraceContainer empty_trace()
Definition fixtures.cpp:153
AvmFlavorSettings::FF FF
Definition field.hpp:10