Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
poseidon2.quad_internal_soundness.test.cpp
Go to the documentation of this file.
1// Regression tests for the Mega Poseidon2 compressed internal relations and their boundaries.
2//
3// All five Poseidon2 gate kinds share the single `poseidon2` block, emitted contiguously as
4// initial | g1 external x4 | transition_entry | quad x13 | terminal | g2 external x4 | out
5// so rows are located by selector (not by hardcoded index). The compressed block has an entry
6// transition row (standard -> compressed) and a terminal row (compressed -> first final-external
7// row); both are tied to the surrounding standard-encoded states via shifted wires:
8//
9// - Entry (q_poseidon2_transition_entry):
10// w_r_shift - D_1 (w_l + q_l)^5 - w_r - w_o - w_4 = 0
11// ties the first compressed row's `w_r` (= state[0] one round ahead) to the standard `s_1`
12// at round `rounds_f_begin`.
13//
14// - Terminal (q_poseidon2_quad_internal_terminal):
15// out_k - w_{k,shift} = 0 for k in {0, 1, 2, 3}
16// ties the compressed chain's computed state at round `p_end` directly to the first
17// final-external row (the rows are contiguous, so its w_shift lands on the real consumer).
18//
19// CircuitChecker iterates row-major-then-relation-major and short-circuits on the first
20// failing relation. This means a corruption that would in principle break multiple relations
21// is reported as breaking the first one the checker reaches; the tests below note the
22// expected first-detector where it matters. To pin down which relation a tamper actually
23// breaks (rather than relying only on the aggregate `CircuitChecker::check` verdict), each
24// test also evaluates the relevant Poseidon2 relation in isolation at the boundary row via
25// `relation_fires`, asserting it is satisfied on the honest circuit and violated after the
26// tamper.
27
39
40#include <gtest/gtest.h>
41
42using namespace bb;
43
44namespace {
45
46class Poseidon2QuadInternalSoundnessTests : public ::testing::Test {
47 public:
49 using FF = MegaFlavor::FF;
50
51 // Locate the (unique, for a single permutation) row carrying a given gate selector in the
52 // merged `poseidon2` block. Rows are interleaved (initial / external / transition / quad /
53 // terminal), so tests must find their target by selector rather than by a fixed index.
54 static size_t selector_row(const Builder& b, GateKind kind)
55 {
56 const auto& block = b.blocks.poseidon2;
57 for (size_t i = 0; i < block.size(); ++i) {
58 if (!read_gate_selector(block, kind, i).is_zero()) {
59 return i;
60 }
61 }
62 throw_or_abort("selector not found in poseidon2 block");
63 return 0;
64 }
65
66 // True iff `Relation` is violated at row `idx` of the `poseidon2` block.
67 template <typename Relation> static bool relation_fires(Builder& builder, size_t idx)
68 {
69 return !UltraCircuitChecker::check_relation_at_row<Relation>(builder, builder.blocks.poseidon2, idx);
70 }
71
72 // Build an honest Poseidon2 circuit: hashes a single fixed field element through the
73 // `Poseidon2Permutation::permutation` call used by the stdlib.
74 static std::unique_ptr<Builder> build_honest_permutation(const FF& input_value)
75 {
76 auto builder = std::make_unique<Builder>(std::make_shared<ECCOpQueue>(), /*is_write_vk_mode=*/true);
78 State input{
83 };
85 return builder;
86 }
87};
88
89TEST_F(Poseidon2QuadInternalSoundnessTests, DoesNotMaterializeUnusedNonTerminalStateLimbs)
90{
91 auto builder = std::make_unique<Builder>(std::make_shared<ECCOpQueue>(), /*is_write_vk_mode=*/true);
92 const size_t initial_num_variables = builder->get_num_variables();
93
95 State input{
100 };
102
103 // Initial input witnesses + initial-linear-layer output + first-half external-round outputs +
104 // compressed internal witnesses + final-half external-round outputs.
105 //
106 // The non-terminal compressed rows only need state[0] at the next row; state[1..3] are derived by
107 // the relation and are not materialized until the terminal row bridges back to standard encoding.
108 constexpr size_t input_witnesses = 4;
109 constexpr size_t initial_external_output_witnesses = 4;
110 constexpr size_t external_output_witnesses = 8 * 4;
111 constexpr size_t compressed_intermediate_witnesses = 14 * 3;
112 constexpr size_t compressed_next_state_zero_witnesses = 14;
113 constexpr size_t compressed_terminal_standard_limbs = 3;
114 constexpr size_t expected_num_variables = input_witnesses + initial_external_output_witnesses +
115 external_output_witnesses + compressed_intermediate_witnesses +
116 compressed_next_state_zero_witnesses + compressed_terminal_standard_limbs;
117
118 EXPECT_EQ(builder->get_num_variables() - initial_num_variables, expected_num_variables);
119 EXPECT_TRUE(CircuitChecker::check(*builder));
120}
121
122// Entry boundary: tampering the first compressed row's `w_r` (= intermediate_s0) breaks the
123// entry-transition relation, which enforces
124// w_r_shift = D_1 (s_0 + c)^5 + s_1 + s_2 + s_3
125// on the entry row (w_r_shift lives in the first compressed row and is the tampered witness).
126TEST_F(Poseidon2QuadInternalSoundnessTests, EntryBoundaryRejectsTamperedIntermediateS0)
127{
128 auto builder = build_honest_permutation(FF(uint256_t(0x1234ULL)));
129 ASSERT_TRUE(CircuitChecker::check(*builder));
130
131 auto& quad = builder->blocks.poseidon2;
132 const size_t entry_row = selector_row(*builder, GateKind::Poseidon2TransitionEntry);
133 // The first interior compressed row is the entry transition's immediate successor.
134 const size_t first_interior_row = entry_row + 1;
135 ASSERT_FALSE(relation_fires<Poseidon2TransitionEntryRelation<FF>>(*builder, entry_row));
136
137 const uint32_t w_r_idx = quad.w_r()[first_interior_row];
138 builder->set_variable(w_r_idx, builder->get_variable(w_r_idx) + FF(1));
139
140 EXPECT_FALSE(CircuitChecker::check(*builder));
141 // w_r_shift of the entry row is the first interior row's w_r, so subrelation A_0 breaks.
142 EXPECT_TRUE(relation_fires<Poseidon2TransitionEntryRelation<FF>>(*builder, entry_row));
143}
144
145// Entry boundary: tampering the entry row's own `w_r` (= standard s_1 at round rounds_f_begin)
146// breaks the entry-transition relation as well. `w_r` of the entry row shares its witness index
147// with the last first-group external round's output, so modifying it invalidates both the
148// external chain (which pins it via w_shift) and the entry relation that reads it.
149TEST_F(Poseidon2QuadInternalSoundnessTests, EntryBoundaryRejectsTamperedStateOne)
150{
151 auto builder = build_honest_permutation(FF(uint256_t(0xabcdULL)));
152 ASSERT_TRUE(CircuitChecker::check(*builder));
153
154 auto& quad = builder->blocks.poseidon2;
155 const size_t entry_row = selector_row(*builder, GateKind::Poseidon2TransitionEntry);
156 ASSERT_FALSE(relation_fires<Poseidon2TransitionEntryRelation<FF>>(*builder, entry_row));
157
158 const uint32_t w_r_idx = quad.w_r()[entry_row];
159 builder->set_variable(w_r_idx, builder->get_variable(w_r_idx) + FF(7));
160
161 EXPECT_FALSE(CircuitChecker::check(*builder));
162 // The entry relation reads the entry row's own w_r (the standard s_1), so it breaks.
163 EXPECT_TRUE(relation_fires<Poseidon2TransitionEntryRelation<FF>>(*builder, entry_row));
164}
165
166// Exit boundary: the terminal relation's successor is the first final-external row, which holds
167// the full standard state (s_0, s_1, s_2, s_3) at round p_end. Its `w_r` (= state[1]) is bound by
168// the terminal subrelation out_1 == w_r_shift. Shifting that witness must be rejected.
169TEST_F(Poseidon2QuadInternalSoundnessTests, ExitBoundaryRejectsTamperedStateOne)
170{
171 auto builder = build_honest_permutation(FF(uint256_t(0xcafebabeULL)));
172 ASSERT_TRUE(CircuitChecker::check(*builder));
173
174 auto& quad = builder->blocks.poseidon2;
175 const size_t terminal_row = selector_row(*builder, GateKind::Poseidon2QuadIntTerminal);
176 const size_t first_final_external_row = terminal_row + 1;
177 ASSERT_FALSE(relation_fires<Poseidon2QuadInternalTerminalRelation<FF>>(*builder, terminal_row));
178
179 const uint32_t state1_idx = quad.w_r()[first_final_external_row];
180 builder->set_variable(state1_idx, builder->get_variable(state1_idx) + FF(1));
181
182 EXPECT_FALSE(CircuitChecker::check(*builder));
183 // The terminal relation's subrelation out_1 == w_r_shift binds this witness, so it breaks.
184 EXPECT_TRUE(relation_fires<Poseidon2QuadInternalTerminalRelation<FF>>(*builder, terminal_row));
185}
186
187// Interior chain: corrupting any wire on an interior compressed row breaks the chain's
188// quad-internal relation locally.
189TEST_F(Poseidon2QuadInternalSoundnessTests, InteriorRelationRejectsTamperedWire)
190{
191 auto builder = build_honest_permutation(FF(uint256_t(0xfeedf00dULL)));
192 ASSERT_TRUE(CircuitChecker::check(*builder));
193
194 auto& quad = builder->blocks.poseidon2;
195 // Pick some middle interior row (first interior row is the entry transition's successor).
196 const size_t interior_row = selector_row(*builder, GateKind::Poseidon2TransitionEntry) + 1 + 5;
197 ASSERT_FALSE(relation_fires<Poseidon2QuadInternalRelation<FF>>(*builder, interior_row));
198
199 const uint32_t w_o_idx = quad.w_o()[interior_row];
200 builder->set_variable(w_o_idx, builder->get_variable(w_o_idx) + FF(1));
201
202 EXPECT_FALSE(CircuitChecker::check(*builder));
203 // The quad-internal relation reads this row's own w_o, so the interior chain breaks here.
204 EXPECT_TRUE(relation_fires<Poseidon2QuadInternalRelation<FF>>(*builder, interior_row));
205}
206
207// Cross-row encoding test: the interior subrelations A_1, A_2, A_3 compare row i's predicted
208// (out_1, out_2, out_3) against row i+1's reconstructed Vandermonde encoding (b_1', b_2',
209// b_3'), where b_k' is built from row i+1's lane-0 chain. Tampering an interior row's wire
210// perturbs that reconstruction at the *previous* row without touching the previous row's
211// own committed wires — exercising the bijectivity-of-V mechanism that lets the relation
212// compare uncommitted hidden lanes. The tampered wire is also row i+1's own committed wire,
213// so the tamper would also break row i+1's own relation; CircuitChecker may report either
214// site, but both are exercising the same Vandermonde-encoding equality.
215TEST_F(Poseidon2QuadInternalSoundnessTests, CrossRowVandermondeEncodingMismatchRejected)
216{
217 auto builder = build_honest_permutation(FF(uint256_t(0xCAFE1234ULL)));
218 ASSERT_TRUE(CircuitChecker::check(*builder));
219
220 auto& quad = builder->blocks.poseidon2;
221 const size_t row_i_plus_1 = selector_row(*builder, GateKind::Poseidon2TransitionEntry) + 1 + 6;
222 const size_t row_i = row_i_plus_1 - 1;
223 ASSERT_FALSE(relation_fires<Poseidon2QuadInternalRelation<FF>>(*builder, row_i));
224
225 const uint32_t idx = quad.w_o()[row_i_plus_1];
226 builder->set_variable(idx, builder->get_variable(idx) + FF(1));
227
228 EXPECT_FALSE(CircuitChecker::check(*builder));
229 // Row i's committed wires are untouched, but row i+1's w_o enters row i's relation as w_o_shift
230 // through the Vandermonde RHS reconstruction (b_2', b_3'), so row i's relation fires.
231 EXPECT_TRUE(relation_fires<Poseidon2QuadInternalRelation<FF>>(*builder, row_i));
232}
233
234} // namespace
Curve::ScalarField FF
A wrapper for Relations to expose methods used by the Sumcheck prover or verifier to add the contribu...
static bool check(const Builder &circuit)
Check the witness satisifies the circuit.
Circuit form of Poseidon2 permutation from https://eprint.iacr.org/2023/323.
std::array< field_t< Builder >, t > State
AluTraceBuilder builder
Definition alu.test.cpp:124
FF b
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
TEST_F(IPATest, ChallengesAreZero)
Definition ipa.test.cpp:160
MegaCircuitBuilder_< field< Bn254FrParams > > MegaCircuitBuilder
FF read_gate_selector(const ExecutionTraceBlock< FF, NUM_WIRES > &block, GateKind kind, size_t idx)
Gate-selector value at (block, idx) for kind, returning zero if the block does not own this kind or t...
GateKind
Tag identifying which gate selector a block owns. Used by cross-block readers to decide whether (bloc...
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
static constexpr field zero()
void throw_or_abort(std::string const &err)