Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
poseidon2.external_internal_transitions.test.cpp
Go to the documentation of this file.
1// Malicious-prover soundness tests for the external<->internal transitions in the Mega Poseidon2 block.
2//
3// All five Poseidon2 gate kinds (external, external-initial, quad-internal, terminal, transition-entry)
4// share the single `poseidon2` block, so each permutation's rows are contiguous:
5//
6// initial | g1 external x4 | transition_entry | quad x13 | terminal | g2 external x4 | output
7//
8// Contiguity lets the `v_k = w_shift` round relations bind directly across the external<->internal
9// boundary, so the only selector-unconstrained row per permutation is the final `output` landing.
10//
11// Soundness requires every boundary-handoff witness to be pinned by a relation. Each test tampers every
12// wire of a boundary row and asserts CircuitChecker rejects the result:
13// - the `transition_entry` state is pinned by the last first-group external round's relation, whose
14// w_shift targets this row;
15// - the first final-group external round (`g2r0`) holds the full standard state at round p_end, pinned
16// by the terminal relation, whose w_shift targets this row;
17// - the final `output` row is pinned by the last external round's relation, whose w_shift targets it.
18//
19// These wires are shared witnesses. Beyond asserting CircuitChecker rejects the tamper, each test pins
20// the failure to the specific producing relation whose w_shift targets the row (via relation_fires), so a
21// pass proves that relation -- not merely some incidental constraint -- holds the boundary wire. A
22// genuinely free wire would leave the tampered circuit valid and the relation satisfied, failing both
23// assertions: an unconstrained boundary wire would let a prover forge the permutation output.
24
35
36#include <gtest/gtest.h>
37
38using namespace bb;
39
40namespace {
41
42class Poseidon2ExternalInternalTransitionsTests : public ::testing::Test {
43 public:
45 using FF = MegaFlavor::FF;
46
47 static std::unique_ptr<Builder> build_honest_permutation(const FF& input_value)
48 {
49 auto builder = std::make_unique<Builder>(std::make_shared<ECCOpQueue>(), /*is_write_vk_mode=*/true);
51 State input{
56 };
58 return builder;
59 }
60
61 static size_t selector_row(const Builder& b, GateKind kind)
62 {
63 const auto& block = b.blocks.poseidon2;
64 for (size_t i = 0; i < block.size(); ++i) {
65 if (!read_gate_selector(block, kind, i).is_zero()) {
66 return i;
67 }
68 }
69 throw_or_abort("selector not found");
70 return 0;
71 }
72
73 // First external round of the final group: the first Poseidon2Ext row after the terminal row.
74 static size_t final_group_first_external_row(const Builder& b)
75 {
76 const auto& block = b.blocks.poseidon2;
77 const size_t terminal = selector_row(b, GateKind::Poseidon2QuadIntTerminal);
78 for (size_t i = terminal + 1; i < block.size(); ++i) {
79 if (!read_gate_selector(block, GateKind::Poseidon2Ext, i).is_zero()) {
80 return i;
81 }
82 }
83 throw_or_abort("no final-group external row");
84 return 0;
85 }
86
87 // True iff `Relation` is violated at row `idx` of the `poseidon2` block.
88 template <typename Relation> static bool relation_fires(Builder& builder, size_t idx)
89 {
90 return !UltraCircuitChecker::check_relation_at_row<Relation>(builder, builder.blocks.poseidon2, idx);
91 }
92
93 // Tamper `wire` of `tamper_row` and assert both that CircuitChecker rejects the circuit and that the
94 // specific pinning `Relation` at `relation_row` -- whose w_shift targets the tampered row -- now fires.
95 template <typename Relation>
96 static void expect_tamper_rejected(const FF& input, size_t tamper_row, size_t wire, size_t relation_row)
97 {
98 auto builder = build_honest_permutation(input);
99 ASSERT_TRUE(CircuitChecker::check(*builder));
100 ASSERT_FALSE(relation_fires<Relation>(*builder, relation_row));
101
102 auto& block = builder->blocks.poseidon2;
103 const uint32_t idx = block.wires[wire][tamper_row];
104 builder->set_variable(idx, builder->get_variable(idx) + FF(1));
105
106 EXPECT_FALSE(CircuitChecker::check(*builder))
107 << "tampering wire " << wire << " of boundary row " << tamper_row << " was NOT rejected";
108 EXPECT_TRUE(relation_fires<Relation>(*builder, relation_row))
109 << "tampering wire " << wire << " of boundary row " << tamper_row
110 << " did not break the pinning relation at row " << relation_row;
111 }
112};
113
114TEST_F(Poseidon2ExternalInternalTransitionsTests, AllGateKindsShareOneBlock)
115{
116 auto builder = build_honest_permutation(FF(uint256_t(0x1234ULL)));
117 ASSERT_TRUE(CircuitChecker::check(*builder));
118 EXPECT_GT(builder->blocks.poseidon2.size(), 0u);
119 for (const GateKind kind : { GateKind::Poseidon2Ext,
120 GateKind::Poseidon2ExtInitial,
121 GateKind::Poseidon2QuadInt,
122 GateKind::Poseidon2QuadIntTerminal,
123 GateKind::Poseidon2TransitionEntry }) {
124 EXPECT_NO_THROW(selector_row(*builder, kind)) << "gate kind missing from poseidon2 block";
125 }
126}
127
128// Boundary 1: the entry handoff. The transition-entry state is pinned by the last first-group external
129// round's relation, whose w_shift targets this row; tampering any of its wires must be rejected.
130TEST_F(Poseidon2ExternalInternalTransitionsTests, EntryHandoffTamperRejected)
131{
132 const FF input(uint256_t(0xabcdULL));
133 const size_t row = selector_row(*build_honest_permutation(input), GateKind::Poseidon2TransitionEntry);
134 // The preceding external round's w_shift targets the entry row, so its relation pins these wires.
135 for (size_t wire = 0; wire < Builder::ExecutionTrace::NUM_WIRES; ++wire) {
136 expect_tamper_rejected<Poseidon2ExternalRelation<FF>>(input, row, wire, row - 1);
137 }
138}
139
140// Boundary 2: the exit handoff. The first final-group external round (g2r0) holds the full standard
141// state at round p_end, pinned by the terminal relation whose w_shift targets this row; tampering any
142// of its wires must be rejected.
143TEST_F(Poseidon2ExternalInternalTransitionsTests, ExitHandoffTamperRejected)
144{
145 const FF input(uint256_t(0xfeedULL));
146 auto builder = build_honest_permutation(input);
147 const size_t row = final_group_first_external_row(*builder);
148 const size_t terminal_row = selector_row(*builder, GateKind::Poseidon2QuadIntTerminal);
149 // The terminal relation's w_shift targets g2r0 (out_k == w_*_shift), pinning its full standard state.
150 for (size_t wire = 0; wire < Builder::ExecutionTrace::NUM_WIRES; ++wire) {
151 expect_tamper_rejected<Poseidon2QuadInternalTerminalRelation<FF>>(input, row, wire, terminal_row);
152 }
153}
154
155// The only selector-unconstrained row -- the final external round's output landing (last row of the
156// block) -- is pinned by that round's relation, whose w_shift targets it.
157TEST_F(Poseidon2ExternalInternalTransitionsTests, FinalOutputRowTamperRejected)
158{
159 const FF input(uint256_t(0xCAFEBABEULL));
160 const size_t row = build_honest_permutation(input)->blocks.poseidon2.size() - 1;
161 // The last external round's w_shift targets the output row, so its relation pins these wires.
162 for (size_t wire = 0; wire < Builder::ExecutionTrace::NUM_WIRES; ++wire) {
163 expect_tamper_rejected<Poseidon2ExternalRelation<FF>>(input, row, wire, row - 1);
164 }
165}
166
167} // namespace
Curve::ScalarField FF
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)