Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
poseidon2_quad_closed_form.test.cpp
Go to the documentation of this file.
1// Equivalence test: closed-form 4-round propagation vs. step iteration.
2//
3// The K=4 compressed Poseidon2 internal-round relation currently computes (out_0..out_3) from
4// (w_l, w_r, w_o, w_4, q_l..q_4) via:
5// 1) S-boxes u_k = (w_k + q_k)^5 for k = 0..3
6// 2) Vandermonde RHS b_1, b_2, b_3 (linear in w_r, w_o, w_4 and u_0, u_1, u_2)
7// 3) Lagrange solve s_j^{(0)} = α_j^(1) b_1 + α_j^(2) b_2 + α_j^(3) b_3
8// 4) Four step iterations of the internal-MDS update on (s_1, s_2, s_3)
9// 5) out_0 = D_1 u_3 + T_3, out_{1,2,3} = state[1..3] at round 4
10//
11// Steps 2..5 are linear in (w_r, w_o, w_4, u_0..u_3) (everything is linear once the four S-boxes
12// are taken as opaque inputs). They can be folded into a single linear map. This test verifies
13// that the coefficient tables consumed by the relations agree with explicit step iteration on
14// random inputs.
15
18
19#include <array>
20#include <gtest/gtest.h>
21
22namespace {
23
24using FF = bb::fr;
26
27struct Out {
28 FF out_0, out_1, out_2, out_3;
29};
30
31// Reference: same body as Poseidon2QuadInternalRelationImpl::accumulate, evaluated
32// in plain field arithmetic.
33Out reference_step_iter(FF s1, FF s2, FF s3, FF u0, FF u1, FF u2, FF u3)
34{
35 auto step = [](FF& x1, FF& x2, FF& x3, const FF& u) {
36 FF sum = x1 + x2 + x3;
37 FF t = u + sum;
38 FF n1 = t + x1 * (QuadParams::D2 - FF(1));
39 FF n2 = t + x2 * (QuadParams::D3 - FF(1));
40 FF n3 = t + x3 * (QuadParams::D4 - FF(1));
41 x1 = n1;
42 x2 = n2;
43 x3 = n3;
44 };
45 step(s1, s2, s3, u0);
46 step(s1, s2, s3, u1);
47 step(s1, s2, s3, u2);
48 FF T_3 = s1 + s2 + s3;
49 FF out_0 = u3 * QuadParams::D1 + T_3;
50 step(s1, s2, s3, u3);
51 return { out_0, s1, s2, s3 };
52}
53
54Out closed_form(FF w_r, FF w_o, FF w_4, FF u0, FF u1, FF u2, FF u3)
55{
56 const auto& table = QuadParams::tables.closed_form;
57 auto eval = [&](size_t row) {
58 return table[row][0] * w_r + table[row][1] * w_o + table[row][2] * w_4 + table[row][3] * u0 +
59 table[row][4] * u1 + table[row][5] * u2 + table[row][6] * u3;
60 };
61 return { eval(0), eval(1), eval(2), eval(3) };
62}
63
64// Reference path: derive (s_1^{(0)}, s_2^{(0)}, s_3^{(0)}) from (w_*, u_*) via the b_k formulas
65// + Lagrange solve, then iterate the internal-MDS steps explicitly.
66Out reference_from_wires(FF w_r, FF w_o, FF w_4, FF u0, FF u1, FF u2, FF u3)
67{
68 const FF D1 = QuadParams::D1;
69 const FF S = QuadParams::SIGMA;
70 FF b_1 = w_r - D1 * u0;
71 FF b_2 = -FF(2) * w_r + w_o + (FF(2) * D1 - FF(3)) * u0 - D1 * u1;
72 FF b_3 = -(S + FF(2)) * w_r - w_o + w_4 + ((S + FF(2)) * D1 - S - FF(3)) * u0 + (D1 - FF(3)) * u1 - D1 * u2;
73 FF s1 = QuadParams::alpha_1_1 * b_1 + QuadParams::alpha_1_2 * b_2 + QuadParams::alpha_1_3 * b_3;
74 FF s2 = QuadParams::alpha_2_1 * b_1 + QuadParams::alpha_2_2 * b_2 + QuadParams::alpha_2_3 * b_3;
75 FF s3 = QuadParams::alpha_3_1 * b_1 + QuadParams::alpha_3_2 * b_2 + QuadParams::alpha_3_3 * b_3;
76 return reference_step_iter(s1, s2, s3, u0, u1, u2, u3);
77}
78
79} // namespace
80
81TEST(Poseidon2QuadClosedForm, ForwardVandermondeLhsMatchesWeightedSum)
82{
83 // Sanity: each forward-Vandermonde LHS row should equal the weighted sum of the
84 // out_1, out_2, out_3 closed-form rows.
85 const std::array<std::array<FF, 3>, 3> weights = {
86 { { FF(1), FF(1), FF(1) },
87 { QuadParams::D2, QuadParams::D3, QuadParams::D4 },
88 { QuadParams::D2 * QuadParams::D2, QuadParams::D3 * QuadParams::D3, QuadParams::D4 * QuadParams::D4 } }
89 };
90 for (size_t k = 0; k < 3; ++k) {
91 for (size_t i = 0; i < 7; ++i) {
92 FF expected = weights[k][0] * QuadParams::tables.closed_form[1][i] +
93 weights[k][1] * QuadParams::tables.closed_form[2][i] +
94 weights[k][2] * QuadParams::tables.closed_form[3][i];
95 EXPECT_EQ(QuadParams::tables.forward_vandermonde_lhs[k][i], expected) << "row " << k << " col " << i;
96 }
97 }
98}
99
100TEST(Poseidon2QuadClosedForm, MatchesStepIteration)
101{
102 for (int trial = 0; trial < 100; ++trial) {
103 FF w_r = FF::random_element();
104 FF w_o = FF::random_element();
105 FF w_4 = FF::random_element();
106 FF u0 = FF::random_element();
107 FF u1 = FF::random_element();
108 FF u2 = FF::random_element();
109 FF u3 = FF::random_element();
110
111 Out ref = reference_from_wires(w_r, w_o, w_4, u0, u1, u2, u3);
112 Out cf = closed_form(w_r, w_o, w_4, u0, u1, u2, u3);
113
114 EXPECT_EQ(ref.out_0, cf.out_0) << "trial " << trial;
115 EXPECT_EQ(ref.out_1, cf.out_1) << "trial " << trial;
116 EXPECT_EQ(ref.out_2, cf.out_2) << "trial " << trial;
117 EXPECT_EQ(ref.out_3, cf.out_3) << "trial " << trial;
118 }
119}
120
121// The terminal relation `Poseidon2QuadInternalTerminalRelationImpl::accumulate` exploits the
122// fact that the U_3 coefficient of out_1, out_2, out_3 in the closed-form table is identically
123// 1, allowing it to write `+ u_3` instead of `+ u_3 * C[k][6]` (lines 85-87 of
124// poseidon2_quad_internal_terminal_relation.hpp). This invariant is hard-coded by `build_out_j`
125// (`r[U_3] = FF(1)`); if a future refactor removes that line, the terminal relation becomes
126// silently incorrect. This sentinel test pins the invariant.
127TEST(Poseidon2QuadClosedForm, TerminalU3CoefIsOne)
128{
129 EXPECT_EQ(QuadParams::tables.closed_form[QuadParams::OUT_1][QuadParams::U_3], FF(1));
130 EXPECT_EQ(QuadParams::tables.closed_form[QuadParams::OUT_2][QuadParams::U_3], FF(1));
131 EXPECT_EQ(QuadParams::tables.closed_form[QuadParams::OUT_3][QuadParams::U_3], FF(1));
132}
133
134// Edge-case adversarial inputs. The 100 random trials in `MatchesStepIteration` are
135// statistically sufficient (false-positive prob ~ 100/p), but they may miss bugs that only
136// manifest at special field-arithmetic boundary values. This test exercises:
137// - All-zero inputs (a fixed point of the linear dynamics).
138// - 1 and -1 (smallest nonzero values).
139// - Inputs producing u_k = 0 for various k (testing the "driver = 0" sub-cases).
140// - p/2 and other large values (exercising any path that branches on representation).
141TEST(Poseidon2QuadClosedForm, MatchesStepIterationOnEdgeCases)
142{
143 const FF zero = FF::zero();
144 const FF one = FF(1);
145 const FF neg_one = -FF(1);
146 const FF p_half = -(one * (FF(2).invert())); // (-1)/2 = (p-1)/2 mod p
147
148 struct Trial {
149 const char* name;
150 FF w_r, w_o, w_4, u0, u1, u2, u3;
151 };
152
153 const std::array<Trial, 8> trials = { {
154 { "all_zero", zero, zero, zero, zero, zero, zero, zero },
155 { "all_one", one, one, one, one, one, one, one },
156 { "all_neg_one", neg_one, neg_one, neg_one, neg_one, neg_one, neg_one, neg_one },
157 { "p_half", p_half, p_half, p_half, p_half, p_half, p_half, p_half },
158 { "u0_zero", one, one, one, zero, one, one, one },
159 { "u3_zero", one, one, one, one, one, one, zero },
160 { "all_u_zero", FF::random_element(), FF::random_element(), FF::random_element(), zero, zero, zero, zero },
161 { "all_w_zero",
162 zero,
163 zero,
164 zero,
169 } };
170
171 for (const auto& t : trials) {
172 Out ref = reference_from_wires(t.w_r, t.w_o, t.w_4, t.u0, t.u1, t.u2, t.u3);
173 Out cf = closed_form(t.w_r, t.w_o, t.w_4, t.u0, t.u1, t.u2, t.u3);
174
175 EXPECT_EQ(ref.out_0, cf.out_0) << "trial " << t.name;
176 EXPECT_EQ(ref.out_1, cf.out_1) << "trial " << t.name;
177 EXPECT_EQ(ref.out_2, cf.out_2) << "trial " << t.name;
178 EXPECT_EQ(ref.out_3, cf.out_3) << "trial " << t.name;
179 }
180}
bb::field< bb::Bn254FrParams > FF
Definition field.cpp:24
field< Bn254FrParams > fr
Definition fr.hpp:155
Inner sum(Cont< Inner, Args... > const &in)
Definition container.hpp:70
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
TEST(Poseidon2QuadClosedForm, ForwardVandermondeLhsMatchesWeightedSum)
constexpr field invert() const noexcept
static field random_element(numeric::RNG *engine=nullptr) noexcept