Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
poseidon2.test.cpp
Go to the documentation of this file.
1#include <gmock/gmock.h>
2#include <gtest/gtest.h>
3
4#include <cstdint>
5
27
28// Temporary imports, see comment in test.
31
32// Imports for class_id_derivation-based vulnerability test
40
41namespace bb::avm2::constraining {
42
43using ::testing::ElementsAreArray;
44using ::testing::NiceMock;
45using ::testing::Return;
46using ::testing::ReturnRef;
47
48using tracegen::Poseidon2TraceBuilder;
49using tracegen::TestTraceContainer;
50
52using C = Column;
56
70
91
93{
94 check_relation<poseidon2_hash>(testing::empty_trace());
95 check_relation<poseidon2_perm>(testing::empty_trace());
96 check_relation<poseidon2_mem>(testing::empty_trace());
97}
98
99// These tests imports a bunch of external code since hand-generating the poseidon2 trace is a bit laborious atm.
101{
102 // Taken From barretenberg/crypto/poseidon2/poseidon2.test.cpp
103 FF a("9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
104 FF b("9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
105 FF c("0x9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
106 FF d("0x9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
107
108 std::array<FF, 4> input = { a, b, c, d };
109 auto result = poseidon2.permutation(input);
110
111 std::array<FF, 4> expected = {
112 FF("0x2bf1eaf87f7d27e8dc4056e9af975985bccc89077a21891d6c7b6ccce0631f95"),
113 FF("0x0c01fa1b8d0748becafbe452c0cb0231c38224ea824554c9362518eebdd5701f"),
114 FF("0x018555a8eb50cf07f64b019ebaf3af3c925c93e631f3ecd455db07bbb52bbdd3"),
115 FF("0x0cbea457c91c22c6c31fd89afd2541efc2edf31736b9f721e823b2165c90fd41"),
116 };
117
118 EXPECT_THAT(result, ElementsAreArray(expected));
119
122
123 builder.process_permutation(perm_event_emitter.dump_events(), trace);
124 EXPECT_EQ(trace.get_num_rows(), 1);
125
126 check_relation<poseidon2_perm>(trace);
127}
128
129TEST_F(Poseidon2ConstrainingTest, HashWithSinglePermutation)
130{
131 FF a("9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
132 FF b("9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
133 FF c("0x9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
134
135 std::vector<FF> input = { a, b, c };
136 poseidon2.hash(input);
137
138 // This first row column is set becuase it is used in the relations for Poseidon2Hash.
139 // This could be replaced by having the precomputed tables in the trace, but currently that would
140 // mean the clk column of length 2^21 -1 will be include :O
142 { { C::precomputed_first_row, 1 } },
143 });
145
146 builder.process_hash(hash_event_emitter.dump_events(), trace);
147 EXPECT_EQ(trace.get_num_rows(), /*start_row=*/1 + 1);
148
149 check_relation<poseidon2_hash>(trace);
150}
151
152TEST_F(Poseidon2ConstrainingTest, HashWithMultiplePermutation)
153{
154 // Taken From barretenberg/crypto/poseidon2/poseidon2.test.cpp
155 FF a("9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
156 FF b("9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
157 FF c("0x9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
158 FF d("0x9a807b615c4d3e2fa0b1c2d3e4f56789fedcba9876543210abcdef0123456789");
159
160 std::vector<FF> input = { a, b, c, d };
161 poseidon2.hash(input);
162
164 { { C::precomputed_first_row, 1 } },
165 });
167
168 builder.process_hash(hash_event_emitter.dump_events(), trace);
169 EXPECT_EQ(trace.get_num_rows(), /*start_row=*/1 + 2);
170
171 check_relation<poseidon2_hash>(trace);
172}
173
174TEST_F(Poseidon2ConstrainingTest, MultipleHashInvocations)
175{
176 std::vector<FF> input = { 1, 2, 3, 4 };
177
178 FF result = poseidon2.hash(input);
180 EXPECT_EQ(result, bb_result);
181
182 result = poseidon2.hash({ result, 1, 2, 3, 4 });
183 bb_result = crypto::Poseidon2<crypto::Poseidon2Bn254ScalarFieldParams>::hash({ bb_result, 1, 2, 3, 4 });
184 EXPECT_EQ(result, bb_result);
185
187 { { C::precomputed_first_row, 1 } },
188 });
190
191 builder.process_hash(hash_event_emitter.dump_events(), trace);
192 builder.process_permutation(perm_event_emitter.dump_events(), trace);
193 EXPECT_EQ(trace.get_num_rows(), /*start_row=*/1 + /*first_invocation=*/2 + /*second_invokcation=*/2);
194
195 check_relation<poseidon2_hash>(trace);
196}
197
198TEST_F(Poseidon2ConstrainingTest, HashPermInteractions)
199{
200 std::vector<FF> input = { 1, 2, 3, 4 };
201
202 poseidon2.hash(input);
203
205 { { C::precomputed_first_row, 1 } },
206 });
208
209 builder.process_hash(hash_event_emitter.dump_events(), trace);
210 builder.process_permutation(perm_event_emitter.dump_events(), trace);
211 check_interaction<Poseidon2TraceBuilder, lookup_poseidon2_hash_poseidon2_perm_settings>(trace);
212
213 EXPECT_EQ(trace.get_num_rows(), /*start_row=*/1 + /*first_invocation=*/2);
214
215 check_relation<poseidon2_hash>(trace);
216 check_relation<poseidon2_perm>(trace);
217 check_all_interactions<Poseidon2TraceBuilder>(trace);
218}
219
220TEST_F(Poseidon2ConstrainingTest, NegativeHashPermInteractions)
221{
222 std::vector<FF> input = { 1, 2, 3, 4 };
223
224 poseidon2.hash(input);
225
227 { { C::precomputed_first_row, 1 } },
228 });
230
231 builder.process_hash(hash_event_emitter.dump_events(), trace);
232
234 (check_interaction<Poseidon2TraceBuilder, lookup_poseidon2_hash_poseidon2_perm_settings>(trace)),
235 "Failed.*POSEIDON2_PERM. Could not find tuple in destination.");
236
237 EXPECT_EQ(trace.get_num_rows(), /*start_row=*/1 + /*first_invocation=*/2);
238
239 check_relation<poseidon2_hash>(trace);
240}
241
243// Memory Aware Poseidon2
245
247 protected:
248 Poseidon2MemoryConstrainingTest() { ON_CALL(memory, get_space_id).WillByDefault(Return(0)); }
249
252
253 NiceMock<MockMemory> memory;
255 MemoryValue::from<FF>(1), MemoryValue::from<FF>(2), MemoryValue::from<FF>(3), MemoryValue::from<FF>(4)
256 };
257
258 uint32_t src_address = 0;
259 uint32_t dst_address = 4;
260};
261
263{
264 // Read 4 inputs
265 EXPECT_CALL(memory, get)
266 .WillOnce(ReturnRef(inputs[0]))
267 .WillOnce(ReturnRef(inputs[1]))
268 .WillOnce(ReturnRef(inputs[2]))
269 .WillOnce(ReturnRef(inputs[3]));
270 EXPECT_CALL(memory, set).Times(4); // Write 4 outputs
271
273
274 builder.process_permutation_with_memory(perm_mem_event_emitter.dump_events(), trace);
275
276 check_relation<poseidon2_mem>(trace);
277}
278
279TEST_F(Poseidon2MemoryConstrainingTest, PermutationMemoryInteractions)
280{
281 // Read 4 inputs
282 EXPECT_CALL(memory, get)
283 .WillOnce(ReturnRef(inputs[0]))
284 .WillOnce(ReturnRef(inputs[1]))
285 .WillOnce(ReturnRef(inputs[2]))
286 .WillOnce(ReturnRef(inputs[3]));
287 EXPECT_CALL(memory, set).Times(4); // Write 4 outputs
288
289 // Expected bb output from inputs = {1, 2, 3 ,4}
290 std::vector<FF> outputs = { FF("0x224785a48a72c75e2cbb698143e71d5d41bd89a2b9a7185871e39a54ce5785b1"),
291 FF("0x225bb800db22c4f4b09ace45cb484d42b0dd7dfe8708ee26aacde6f2c1fb2cb8"),
292 FF("0x1180f4260e60b4264c987b503075ea8374b53ed06c5145f8c21c2aadb5087d21"),
293 FF("0x16c877b5b9c04d873218804ccbf65d0eeb12db447f66c9ca26fec380055df7e9") };
294
295 // Set the execution and gt traces
297 // Row 0
298 {
299 // Execution
300 { C::execution_sel, 1 },
301 { C::execution_sel_exec_dispatch_poseidon2_perm, 1 },
302 { C::execution_rop_0_, src_address },
303 { C::execution_rop_1_, dst_address },
304 // GT - dst out of range check
305 { C::gt_sel, 1 },
306 { C::gt_input_a, dst_address + 3 }, // highest write address is src_address + 3
307 { C::gt_input_b, AVM_HIGHEST_MEM_ADDRESS },
308 { C::gt_res, 0 },
309 },
310 {
311 // GT - src out of range check
312 { C::gt_sel, 1 },
313 { C::gt_input_a, src_address + 3 }, // highest read address is dst_address + 3
314 { C::gt_input_b, AVM_HIGHEST_MEM_ADDRESS },
315 { C::gt_res, 0 },
316 },
317 });
318
319 // Set up memory trace
320 for (uint32_t i = 0; i < inputs.size(); ++i) {
321 // Set memory reads
322 trace.set(C::memory_address, i, src_address + i);
323 trace.set(C::memory_value, i, inputs[i]);
324 trace.set(C::memory_tag, i, static_cast<uint32_t>(inputs[i].get_tag()));
325 trace.set(C::memory_sel, i, 1);
326 // Set memory writes
327 uint32_t write_index = i + static_cast<uint32_t>(inputs.size());
328 trace.set(C::memory_address, write_index, dst_address + i);
329 trace.set(C::memory_value, write_index, outputs[i]);
330 trace.set(C::memory_sel, write_index, 1);
331 trace.set(C::memory_rw, write_index, 1);
332 }
333
335
336 builder.process_permutation_with_memory(perm_mem_event_emitter.dump_events(), trace);
337 builder.process_permutation(perm_event_emitter.dump_events(), trace);
338
339 check_all_interactions<Poseidon2TraceBuilder>(trace);
340 check_relation<poseidon2_mem>(trace);
341}
342
343TEST_F(Poseidon2MemoryConstrainingTest, PermutationMemoryInvalidTag)
344{
345 // Third input is of the wrong type
347 MemoryValue::from<FF>(1), MemoryValue::from<FF>(2), MemoryValue::from<uint64_t>(3), MemoryValue::from<FF>(4)
348 };
349
350 // Still load all the inputs even though there is in invalid tag
351 EXPECT_CALL(memory, get)
352 .WillOnce(ReturnRef(inputs[0]))
353 .WillOnce(ReturnRef(inputs[1]))
354 .WillOnce(ReturnRef(inputs[2]))
355 .WillOnce(ReturnRef(inputs[3]));
356
357 // Set the execution and gt traces
359 // Row 0
360 {
361 // Execution
362 { C::execution_sel, 1 },
363 { C::execution_sel_exec_dispatch_poseidon2_perm, 1 },
364 { C::execution_rop_0_, src_address },
365 { C::execution_rop_1_, dst_address },
366 { C::execution_sel_opcode_error, 1 }, // Invalid tag error
367 // GT - dst out of range check
368 { C::gt_sel, 1 },
369 { C::gt_input_a, dst_address + 3 }, // highest write address is src_address + 3
370 { C::gt_input_b, AVM_HIGHEST_MEM_ADDRESS },
371 { C::gt_res, 0 },
372 },
373 {
374 // GT - src out of range check
375 { C::gt_sel, 1 },
376 { C::gt_input_a, src_address + 3 }, // highest read address is dst_address + 3
377 { C::gt_input_b, AVM_HIGHEST_MEM_ADDRESS },
378 { C::gt_res, 0 },
379 },
380 });
381
382 // Set up memory trace
383 for (uint32_t i = 0; i < inputs.size(); ++i) {
384 // Set memory reads
385 trace.set(C::memory_address, i, src_address + i);
386 trace.set(C::memory_value, i, inputs[i]);
387 trace.set(C::memory_tag, i, static_cast<uint32_t>(inputs[i].get_tag()));
388 trace.set(C::memory_sel, i, 1);
389 }
390
392 "Poseidon2Exception.* input tag is not FF");
393
394 builder.process_permutation_with_memory(perm_mem_event_emitter.dump_events(), trace);
395
396 // Don't expect any permutation events since the memory access was invalid
397 EXPECT_EQ(perm_event_emitter.dump_events().size(), 0);
398
399 check_relation<poseidon2_mem>(trace);
400 check_all_interactions<Poseidon2TraceBuilder>(trace);
401}
402
403TEST_F(Poseidon2MemoryConstrainingTest, PermutationMemoryInvalidAddressRange)
404{
405 uint32_t src_address = static_cast<uint32_t>(AVM_HIGHEST_MEM_ADDRESS - 2);
406
408 // Row 0
409 {
410 // Execution
411 { C::execution_sel, 1 },
412 { C::execution_sel_exec_dispatch_poseidon2_perm, 1 },
413 { C::execution_rop_0_, src_address },
414 { C::execution_rop_1_, dst_address },
415 { C::execution_sel_opcode_error, 1 }, // Invalid address error
416 // GT Subtrace - dst out of range check
417 { C::gt_sel, 1 },
418 { C::gt_input_a, dst_address + 3 }, // highest write address is src_address + 3
419 { C::gt_input_b, AVM_HIGHEST_MEM_ADDRESS },
420 { C::gt_res, 0 },
421 },
422 {
423 // GT Subtrace - src out of range check
424 { C::gt_sel, 1 },
425 { C::gt_input_a, static_cast<uint64_t>(src_address) + 3 }, // highest read address is dst_address + 3
426 { C::gt_input_b, AVM_HIGHEST_MEM_ADDRESS },
427 { C::gt_res, 1 },
428 },
429 });
430
432 "Poseidon2Exception.* src or dst address out of range");
433
435 builder.process_permutation_with_memory(perm_mem_event_emitter.dump_events(), trace);
436
437 // Don't expect any permutation events since the address range was invalid
438 EXPECT_EQ(perm_event_emitter.dump_events().size(), 0);
439
440 check_relation<poseidon2_mem>(trace);
441 check_all_interactions<Poseidon2TraceBuilder>(trace);
442}
443
445// Regression Test: start * (1 - sel) = 0 constraint
447
448// This test verifies that the SELECTOR_ON_START constraint in poseidon2_hash.pil
449// correctly prevents a malicious prover from setting start=1 on inactive rows (sel=0).
450//
451// Previously, this was a vulnerability where a prover could forge hash results by
452// creating rows with start=1, sel=0, and arbitrary inputs/outputs.
453//
454// The fix adds:
455// #[SELECTOR_ON_START]
456// start * (1 - sel) = 0;
457//
458// This mirrors the existing protection for `end` and matches merkle_check.pil.
459TEST_F(Poseidon2ConstrainingTest, StartSelectorIsProtected)
460{
461 // Create a trace where start=1 but sel=0
462 // This represents a forged row that a malicious prover would try to create
464 { { C::precomputed_first_row, 1 } },
465 {
466 // ATTACK ATTEMPT: sel=0 but start=1
467 // This is now BLOCKED by SELECTOR_ON_START constraint
468 { C::poseidon2_hash_sel, 0 },
469 { C::poseidon2_hash_start, 1 },
470 { C::poseidon2_hash_end, 0 },
471 // Arbitrary inputs - attacker would try to claim fake hash
472 { C::poseidon2_hash_input_0, FF(0xDEADBEEF) },
473 { C::poseidon2_hash_input_1, FF(0xCAFEBABE) },
474 { C::poseidon2_hash_input_2, FF(0x12345678) },
475 { C::poseidon2_hash_output, FF(0x999999) },
476 { C::poseidon2_hash_num_perm_rounds_rem, 2 },
477 { C::poseidon2_hash_input_len, 4 },
478 { C::poseidon2_hash_padding, 2 },
479 },
480 });
481
482 // FIX VERIFIED: The SELECTOR_ON_START constraint now catches this attack
484 "SEL_ON_START_OR_END");
485}
486
487// This test verifies that the SELECTOR_ON_START fix blocks the class_id forgery attack.
488// Previously, an attacker could claim (real_artifact, real_private_root, real_bytecode) → FAKE_class_id
489// by forging a start row with sel=0, start=1.
490// Now the fix blocks this by requiring start=1 implies sel=1.
491TEST_F(Poseidon2ConstrainingTest, FakeClassIdAttackIsBlocked)
492{
497
498 // =========================================================================
499 // STEP 1: Define the REAL contract class that the attacker wants to target
500 // =========================================================================
501 FF real_artifact_hash = FF(0x1111);
502 FF real_private_functions_root = FF(0x2222);
503 FF real_bytecode_commitment = FF(0x3333);
504
505 // This is the REAL class_id that should be derived
506 FF real_class_id = poseidon2.hash(
507 { FF(DOM_SEP__CONTRACT_CLASS_ID), real_artifact_hash, real_private_functions_root, real_bytecode_commitment });
508
509 // =========================================================================
510 // STEP 2: Attacker creates a VALID computation using the REAL bytecode
511 // =========================================================================
512 // The attacker chooses arbitrary first-round inputs but uses the REAL bytecode
513 // in the second round. This gives them a valid end row with (real_bytecode, 0, 0).
514 FF attacker_artifact = FF(0xAAAA);
515 FF attacker_private_root = FF(0xBBBB);
516 FF attacker_bytecode = real_bytecode_commitment; // Use the REAL bytecode
517
518 // Compute the FAKE class_id using the attacker's chosen inputs + real bytecode
519 FF fake_class_id =
520 poseidon2.hash({ FF(DOM_SEP__CONTRACT_CLASS_ID), attacker_artifact, attacker_private_root, attacker_bytecode });
521
522 ASSERT_NE(fake_class_id, real_class_id);
523
524 // =========================================================================
525 // STEP 3: Use ClassIdDerivation simulator to generate VALID trace for attacker's computation
526 // =========================================================================
527 EventEmitter<ClassIdDerivationEvent> attacker_class_id_emitter;
528 ClassIdDerivation attacker_class_id_sim(poseidon2, attacker_class_id_emitter);
529
530 ContractClassWithCommitment attacker_klass;
531 attacker_klass.id = fake_class_id;
532 attacker_klass.artifact_hash = attacker_artifact;
533 attacker_klass.private_functions_root = attacker_private_root;
534 attacker_klass.public_bytecode_commitment = attacker_bytecode;
535
536 attacker_class_id_sim.assert_derivation(attacker_klass);
537
538 // =========================================================================
539 // STEP 4: Build the trace with the attacker's VALID computation
540 // =========================================================================
542 { { C::precomputed_first_row, 1 }, { C::precomputed_zero, 0 } },
543 });
544
547
549 class_id_builder.process(attacker_class_id_emitter.dump_events(), trace);
550
551 // =========================================================================
552 // STEP 5: Verify the attacker's computation is valid (before the attack)
553 // =========================================================================
554 check_relation<poseidon2_hash>(trace);
555 check_relation<class_id_derivation>(trace);
556
557 // =========================================================================
558 // STEP 6: ATTACK ATTEMPT - Claim the REAL inputs produce the FAKE class_id
559 // =========================================================================
560 uint32_t class_id_row = 0;
561 trace.set(class_id_row,
562 { {
563 { C::class_id_derivation_sel, 1 },
564 { C::class_id_derivation_class_id, fake_class_id },
565 { C::class_id_derivation_artifact_hash, real_artifact_hash },
566 { C::class_id_derivation_private_functions_root, real_private_functions_root },
567 { C::class_id_derivation_public_bytecode_commitment, real_bytecode_commitment },
568 { C::class_id_derivation_gen_index_contract_class_id, FF(DOM_SEP__CONTRACT_CLASS_ID) },
569 { C::class_id_derivation_const_four, 4 },
570 } });
571
572 // =========================================================================
573 // STEP 7: Add FORGED start row (this is what the fix blocks)
574 // =========================================================================
575 uint32_t forged_row = trace.get_num_rows();
576 trace.set(forged_row,
577 { {
578 { C::poseidon2_hash_sel, 0 }, // INACTIVE
579 { C::poseidon2_hash_start, 1 }, // FORGED start - now blocked by SELECTOR_ON_START!
580 { C::poseidon2_hash_end, 0 },
581 { C::poseidon2_hash_input_0, FF(DOM_SEP__CONTRACT_CLASS_ID) },
582 { C::poseidon2_hash_input_1, real_artifact_hash },
583 { C::poseidon2_hash_input_2, real_private_functions_root },
584 { C::poseidon2_hash_output, fake_class_id },
585 { C::poseidon2_hash_num_perm_rounds_rem, 2 },
586 { C::poseidon2_hash_input_len, 4 },
587 { C::poseidon2_hash_padding, 2 },
588 } });
589
590 // =========================================================================
591 // STEP 8: FIX VERIFIED - The SELECTOR_ON_START constraint blocks the attack
592 // =========================================================================
593 // The forged row has start=1 but sel=0, which violates: start * (1 - sel) = 0
595 "SEL_ON_START_OR_END");
596
597 // class_id_derivation relations still pass (the attack is blocked at the poseidon2_hash level)
598 check_relation<class_id_derivation>(trace);
599
600 // =========================================================================
601 // ATTACK BLOCKED: The class_id forgery is no longer possible
602 // =========================================================================
603 // Previously, an attacker could claim ANY class_id for ANY contract inputs.
604 // Now the SELECTOR_ON_START constraint ensures that start=1 requires sel=1,
605 // meaning all poseidon2 constraints must be satisfied on start rows.
606 // This makes it cryptographically infeasible to forge hash results.
607}
608
609} // namespace bb::avm2::constraining
#define EXPECT_THROW_WITH_MESSAGE(code, expectedMessageRegex)
Definition assert.hpp:193
#define AVM_HIGHEST_MEM_ADDRESS
#define DOM_SEP__CONTRACT_CLASS_ID
EventEmitter< Poseidon2PermutationMemoryEvent > perm_mem_event_emitter
EventEmitter< Poseidon2PermutationEvent > perm_event_emitter
EventEmitter< Poseidon2HashEvent > hash_event_emitter
Poseidon2TraceBuilder poseidon2_builder
ClassIdDerivationTraceBuilder class_id_builder
EventEmitter< Poseidon2HashEvent > hash_event_emitter
EventEmitter< GreaterThanEvent > gt_event_emitter
EventEmitter< Poseidon2PermutationMemoryEvent > perm_mem_event_emitter
EventEmitter< Poseidon2PermutationEvent > perm_event_emitter
DeduplicatingEventEmitter< FieldGreaterThanEvent > field_gt_event_emitter
static constexpr size_t SR_SEL_ON_START_OR_END
void assert_derivation(const ContractClassWithCommitment &klass) override
Computes the contract class ID and emits a ClassIdDerivationEvent. Corresponds to the subtrace class_...
void process(const simulation::EventEmitterInterface< simulation::ClassIdDerivationEvent >::Container &events, TraceContainer &trace)
Process class id derivation events and populate the relevant columns in the trace....
void process_hash(const simulation::EventEmitterInterface< simulation::Poseidon2HashEvent >::Container &hash_events, TraceContainer &trace)
Processes the hash events for the Poseidon2 hash function. It populates the columns for the poseidon2...
void set(Column col, uint32_t row, const FF &value)
static FF hash(const std::vector< FF > &input)
Hashes a vector of field elements.
AluTraceBuilder builder
Definition alu.test.cpp:124
TestTraceContainer trace
FF a
FF b
AvmProvingInputs inputs
TEST_F(AvmRecursiveTests, TwoLayerAvmRecursionFailsWithWrongPIs)
TestTraceContainer empty_trace()
Definition fixtures.cpp:153
AvmFlavorSettings::FF FF
Definition field.hpp:10
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13