Skip to content

Commit b5f7017

Browse files
authored
fix: addresses logic audit findings (#22304)
#### L1: ACIR `num_bits` Validation Status: won't fix It is true that `create_logic_constraint` uses `BB_ASSERT` (which aborts) rather than throwing an exception when `num_bits` is invalid. `BB_ASSERT` is the standard pattern throughout barretenberg for structural invariant checks. Additionally, the `num_bits` value originates from Noir's `IntegerBitSize` enum, which restricts values to {1, 8, 16, 32, 64, 128} at the compiler level, so invalid values cannot reach this code through normal operation. #### L2: OriginTag Provenance Status: fixed 0f3ca1c `create_logic_constraint` was not propagating `OriginTag` when converting constant inputs to witnesses, or when returning the final result. Tags are now copied onto the witness before recursing (for both the left-constant and right-constant paths), and the final accumulated result is tagged with the merged tag of both operands. Note we do not propagate tags to the chunks because these are intermediate circuit variables that are not used outside of logic. #### L3: Builder Context Validation Status: fixed 249bfdc When both inputs are witnesses, the code extracted the builder context from only one operand without verifying both refer to the same builder. Added `validate_context<Builder>(a.get_context(), b.get_context())` to assert consistency, matching the pattern used in other stdlib primitives. #### L4: Write-VK Mode Guard for Constant Inputs Status: fixed 5b3d4b8 In write-VK mode, when both inputs are constants, `create_logic_constraint` returns a constant `field_t`. The ACIR bridge then attempts `assert_equal` against a result witness that holds a dummy zero, causing a spurious failure. The fix uses `builder.set_variable()` to patch the result witness with the correct computed value before the equality check. #### I1: Witness Bounds Checks Are Debug-Only Status: acknowledged, will be fixed later The witness index bounds checks in `get_variable()` use `BB_ASSERT_DEBUG`, which is compiled out in release builds. This is known and it affects every circuit component that calls `get_variable()`, not just the logic gadget. We are discussing this the noir team if it can be exploited in any meaningful way. Regardless, we will enable these asserts in release mode soon after ensuring no significant hit in performance because of the asserts. #### I2: Oversized Plookup Table Status: won't fix The code always uses `UINT32_XOR`/`UINT32_AND` plookup tables even when the last chunk is smaller than 32 bits. Smaller tables (UINT8, UINT16) exist and would use fewer sub-lookups. However, this is a minor optimization that would change the circuit structure, which we want to avoid at this point. The existing explicit range constraint on the last chunk ensures correctness regardless of table size. #### I3: Operator Precedence Bug in Test Status: fixed — 2a597fb The test computed `1 << num_bits - 1` intending "all bits set" (`(1 << num_bits) - 1`), but C++ precedence evaluates it as `1 << (num_bits - 1)` (a single bit). For `num_bits = 8`, this gives `128` instead of `255`. Corrected the test. #### I4: Redundant Range Constraints Status: won't fix Yes there could be cases of redundant range constraints (on the noir side as well as barretenberg) but we prefer to have redundancy over missing a range constraint. This was a good find but we decided to not use the optimisation.
1 parent ba14c17 commit b5f7017

File tree

4 files changed

+76
-3
lines changed

4 files changed

+76
-3
lines changed

barretenberg/cpp/src/barretenberg/dsl/acir_format/logic_constraint.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ void create_logic_gate(Builder& builder,
2323
field_ct right = to_field_ct(b, builder);
2424

2525
field_ct computed_result = bb::stdlib::logic<Builder>::create_logic_constraint(left, right, num_bits, is_xor_gate);
26+
27+
// In write-VK mode the result witness holds a dummy zero. In certain cases, the computed result is non-zero so the
28+
// assert_equal would spuriously fail. Patch the witness value so the downstream assertion sees the correct value.
29+
// Eg. for an XOR gate, if the inputs are constants such that the result is a non-zero constant, the assert_equal
30+
// will fail in write-VK mode since the result witness is initialized to zero.
31+
if (builder.is_write_vk_mode()) {
32+
builder.set_variable(result, computed_result.get_value());
33+
}
34+
2635
field_ct acir_result = field_ct::from_witness_index(&builder, result);
2736
computed_result.assert_equal(acir_result);
2837
}

barretenberg/cpp/src/barretenberg/dsl/acir_format/logic_constraint.test.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ class LogicConstraintTestingFunctions {
7373
return WitnessOrConstant<FF>::from_index(input_index);
7474
};
7575

76-
bb::fr lhs = FF(static_cast<uint256_t>(1) << num_bits - 1); // All bits from 0 to num_bits-1 are set
77-
bb::fr rhs = FF(static_cast<uint256_t>(1) << num_bits - 1); // All bits from 0 to num_bits-1 are set
76+
bb::fr lhs = FF((static_cast<uint256_t>(1) << num_bits) - 1); // All bits from 0 to num_bits-1 are set
77+
bb::fr rhs = FF((static_cast<uint256_t>(1) << num_bits) - 1); // All bits from 0 to num_bits-1 are set
7878
bb::fr result = is_xor_gate ? (static_cast<uint256_t>(lhs) ^ static_cast<uint256_t>(rhs))
7979
: (static_cast<uint256_t>(lhs) & static_cast<uint256_t>(rhs));
8080

barretenberg/cpp/src/barretenberg/stdlib/primitives/logic/logic.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,19 @@ field_t<Builder> logic<Builder>::create_logic_constraint(
5858
Builder* ctx = b.get_context();
5959
uint256_t a_native(a.get_value());
6060
field_pt a_witness = field_pt::from_witness_index(ctx, ctx->put_constant_variable(a_native));
61+
a_witness.set_origin_tag(a.get_origin_tag());
6162
return create_logic_constraint(a_witness, b, num_bits, is_xor_gate, get_chunk);
6263
}
6364

6465
if (!a.is_constant() && b.is_constant()) {
6566
Builder* ctx = a.get_context();
6667
uint256_t b_native(b.get_value());
6768
field_pt b_witness = field_pt::from_witness_index(ctx, ctx->put_constant_variable(b_native));
69+
b_witness.set_origin_tag(b.get_origin_tag());
6870
return create_logic_constraint(a, b_witness, num_bits, is_xor_gate, get_chunk);
6971
}
7072

71-
Builder* ctx = a.get_context();
73+
Builder* ctx = validate_context<Builder>(a.get_context(), b.get_context());
7274

7375
// We slice the input values into 32-bit chunks, and then use a multi-table lookup to compute the AND or XOR
7476
// of each chunk. Since we perform the lookup from 32-bit multi-tables, the lookup operation implicitly enforces a
@@ -111,6 +113,7 @@ field_t<Builder> logic<Builder>::create_logic_constraint(
111113
a.assert_equal(a_accumulator, "stdlib logic: failed to reconstruct left operand");
112114
b.assert_equal(b_accumulator, "stdlib logic: failed to reconstruct right operand");
113115

116+
res.set_origin_tag(OriginTag(a.get_origin_tag(), b.get_origin_tag()));
114117
return res;
115118
}
116119
template class logic<bb::UltraCircuitBuilder>;

barretenberg/cpp/src/barretenberg/stdlib/primitives/logic/logic.test.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,64 @@ TYPED_TEST(LogicTest, DifferentWitnessSameResult)
143143
bool result = CircuitChecker::check(builder);
144144
EXPECT_EQ(result, false);
145145
}
146+
147+
// Regression test: OriginTag must propagate through all logic constraint paths.
148+
TYPED_TEST(LogicTest, OriginTagPropagation)
149+
{
150+
STDLIB_TYPE_ALIASES
151+
STANDARD_TESTING_TAGS
152+
153+
auto builder = Builder();
154+
uint256_t a_val = 0xAB;
155+
uint256_t b_val = 0xCD;
156+
157+
// Both witnesses
158+
{
159+
field_ct x = witness_ct(&builder, a_val);
160+
field_ct y = witness_ct(&builder, b_val);
161+
x.set_origin_tag(submitted_value_origin_tag);
162+
y.set_origin_tag(challenge_origin_tag);
163+
164+
field_ct and_result = stdlib::logic<Builder>::create_logic_constraint(x, y, 8, false);
165+
field_ct xor_result = stdlib::logic<Builder>::create_logic_constraint(x, y, 8, true);
166+
167+
EXPECT_EQ(and_result.get_origin_tag(), first_two_merged_tag);
168+
EXPECT_EQ(xor_result.get_origin_tag(), first_two_merged_tag);
169+
}
170+
171+
// Left constant, right witness
172+
{
173+
field_ct x_const(&builder, a_val);
174+
field_ct y = witness_ct(&builder, b_val);
175+
x_const.set_origin_tag(submitted_value_origin_tag);
176+
y.set_origin_tag(challenge_origin_tag);
177+
178+
field_ct result = stdlib::logic<Builder>::create_logic_constraint(x_const, y, 8, false);
179+
EXPECT_EQ(result.get_origin_tag(), first_two_merged_tag);
180+
}
181+
182+
// Right constant, left witness
183+
{
184+
field_ct x = witness_ct(&builder, a_val);
185+
field_ct y_const(&builder, b_val);
186+
x.set_origin_tag(submitted_value_origin_tag);
187+
y_const.set_origin_tag(challenge_origin_tag);
188+
189+
field_ct result = stdlib::logic<Builder>::create_logic_constraint(x, y_const, 8, false);
190+
EXPECT_EQ(result.get_origin_tag(), first_two_merged_tag);
191+
}
192+
193+
// Both constants
194+
{
195+
field_ct x_const(&builder, a_val);
196+
field_ct y_const(&builder, b_val);
197+
x_const.set_origin_tag(submitted_value_origin_tag);
198+
y_const.set_origin_tag(challenge_origin_tag);
199+
200+
field_ct result = stdlib::logic<Builder>::create_logic_constraint(x_const, y_const, 8, false);
201+
EXPECT_EQ(result.get_origin_tag(), first_two_merged_tag);
202+
}
203+
204+
bool result = CircuitChecker::check(builder);
205+
EXPECT_EQ(result, true);
206+
}

0 commit comments

Comments
 (0)