Guidelines for handling point-at-infinity in stdlib circuit types. Use when working on serialization, public inputs, or cycle_group/biggroup code.
element_default::element (Ultra arithmetization): Separate _is_infinity bool_ct flag. Coordinates can be arbitrary when infinity is set. Has is_point_at_infinity() returning bool_ct.goblin_element (Mega arithmetization): Represents infinity as (0,0) by construction. No separate infinity flag. No is_point_at_infinity() method on the circuit type. ECCVM enforces the (0,0) convention.element<Builder, Fq, Fr, G> resolves to one or the other via IsGoblinBigGroup.get_value() returning a native affine_element with is_point_at_infinity().cycle_group has _is_infinity bool computed from x^2 + 5*y^2 == 0 (which has no non-trivial solutions in the field, since p == 2 mod 5). For (0,0) this gives 0 == 0, so infinity is auto-detected.
Non-canonical infinity: cycle_group operations (operator+, operator-, dbl, batch_mul) can produce points at infinity with non-canonical coordinates (_is_infinity=true but x,y != 0,0). This happens because the arithmetic uses conditional_assign to avoid division by zero -- the "real" coordinates are garbage when the infinity flag is set.
Observation boundaries: Canonicalization to (0,0) is deferred to these boundaries:
StdlibCodec::serialize_to_fields -- canonicalizes grumpkin_commitment via conditional_assigncycle_group::set_public -- canonicalizes before exposing as public inputscycle_group::operator== and assert_equal -- handles infinity comparison(0,0) via conditional_assign. This IS needed because the IPA::accumulate -> full_verify_recursive path computes G_zero_1 + G_zero_2 * alpha using cycle_group arithmetic, which can produce non-canonical infinity when a malicious prover sends both G_zero values as (0,0).(0,0) infinity; asserts (BB_ASSERT) that infinity points have zero coordinates. All existing code paths (public inputs, transcript) produce canonical (0,0) infinity, so the assert is a safety guard against misuse. No canonicalization is performed (unlike grumpkin_commitment), since there are no available code paths that produce non-canonical bn254 infinity.Trace whether the value comes from:
reconstruct_from_public, or from transcript via receive_from_prover): Coordinates are already canonical (0,0) for infinity. Canonicalization is a no-op.operator+, operator-, dbl, batch_mul): Coordinates may be non-canonical when _is_infinity is true. Canonicalization IS needed.Key production paths for grumpkin commitments through serialize_to_fields:
add_claim_to_hash_buffer (verifier side): Commitment is deserialized from public inputs -> already canonical.accumulate hashing of G_zero: G_zero is deserialized from transcript -> already canonical.full_verify_recursive: Accumulated commitment is the result of cycle_group arithmetic (G_zero_1 + G_zero_2 * alpha) -> may be non-canonical if infinity -> canonicalization needed.flavor.hpp to_field_elements/hash_with_origin_tagging): Commitments are deserialized from fields -> already canonical.For recursive verifier circuits, the circuit must be constructible even with malicious witness values (it just won't be satisfiable). This means:
BB_ASSERT on values a malicious prover can control -- it would crash circuit construction.conditional_assign canonicalization instead, which produces correct circuit constraints regardless of witness values.BB_ASSERT is appropriate for invariants that hold across all existing code paths (e.g., bn254_commitment in serialize_to_fields asserts canonical (0,0) form for infinity, since all paths that can reach it produce canonical infinity).These patterns have caused repeated bugs across biggroup, cycle_group, ECCVM, AVM, and ECDSA:
(0,0)BB's native affine_element uses an internal sentinel for infinity (MSB set on the x coordinate's raw representation, not a valid field element). Noir, AVM, and the transcript convention use (0,0). Any code that reads raw coordinates without checking is_point_at_infinity() (or without calling get_standard_form() on stdlib types) will see sentinel values, not (0,0). Always use the standard-form convention (0,0) at component boundaries.
When doing conditional_assign or conditional_select on points, both the coordinates AND the _is_infinity flag must be selected. Multiple ECDSA and biggroup bugs came from selecting x/y but leaving _is_infinity unchanged.
Performance-optimized ECC (chain_add, Montgomery ladder) assumes inputs are never infinity and never equal. When infinity appears as an intermediate value, these formulas divide by zero or produce wrong results. If a code path can encounter infinity mid-computation, use complete addition (operator+) instead of chain_add_start/chain_add/chain_add_end. This costs ~2% more gates but is correct.
Constructors that accept a direct is_infinity flag can bypass on-curve validation (a point with is_infinity=true but x,y != 0 passes validate_on_curve because the check is skipped for infinity). The 4-argument biggroup constructor with explicit infinity flag is now private. Prefer the 2-argument (x, y) constructor which auto-detects infinity from x == 0 && y == 0.
cycle_group::operator== and assert_equal handle infinity correctly, but raw coordinate comparison does not. Before comparing or hashing point coordinates, ensure infinity points have canonical (0,0) coordinates. The observation boundary pattern (serialize_to_fields, set_public, operator==) exists for this reason.