Skip to main content

BN254 Field Arithmetic

All arithmetic operations in Ligetron are performed over the BN254 scalar field.

API Overview

Ligetron provides three layers of field arithmetic APIs:

  1. C API (bn254fr_t) - Low-level C interface with explicit memory management
  2. C++ Wrapper (bn254fr_class) - Convenient C++ class with automatic memory management and operator overloading
  3. Rust API (Bn254Fr) - Idiomatic Rust interface with automatic memory management

Most examples use the C++ wrapper (bn254fr_class) for C++ code and Bn254Fr for Rust code, as these provide better ergonomics while wrapping the same underlying C API.

Type Definition

The base C type:

typedef struct __bn254fr {
uint64_t __handle;
} bn254fr_t[1];

C++ Wrapper API

For C++ code, use bn254fr_class which provides automatic memory management:

#include <ligetron/bn254fr_class.h>
using namespace ligetron;

// Constructor handles allocation automatically
bn254fr_class a(42); // From integer
bn254fr_class b("0x2a", 16); // From hex string
bn254fr_class c; // Default constructor

// Arithmetic operations
bn254fr_class result;
addmod(result, a, b); // result = a + b mod p
mulmod(result, a, b); // result = a * b mod p

// Operator overloads
if (a == b) { /* ... */ }
if (a < b) { /* ... */ }

// Destructor handles cleanup automatically

Most C++ examples use bn254fr_class for convenience.

Memory Management (C API)

For the low-level C API (bn254fr_t), you must manually manage memory:

// Allocate field element
void bn254fr_alloc(bn254fr_t fr);

// Free field element
void bn254fr_free(bn254fr_t fr);

Important: Always free field elements when done, especially after applying constraints.

Note: The C++ wrapper (bn254fr_class) and Rust API (Bn254Fr) handle memory management automatically.

Initialization

// Set from unsigned integers
void bn254fr_set_u32(bn254fr_t out, uint32_t x);
void bn254fr_set_u64(bn254fr_t out, uint64_t x);

// Set from string (base 10 or 16)
// base: 0 (auto), 10 (decimal), 16 (hexadecimal)
void bn254fr_set_str(bn254fr_t out, const char* str, uint32_t base = 0);

// Set from byte array
// order: -1 (little-endian), 1 (big-endian)
void bn254fr_set_bytes(bn254fr_t out, const unsigned char* bytes,
uint32_t len, int32_t order);

// Helper functions
void bn254fr_set_bytes_little(bn254fr_t out, const unsigned char* bytes, uint32_t len);
void bn254fr_set_bytes_big(bn254fr_t out, const unsigned char* bytes, uint32_t len);

Arithmetic Operations

// Addition: out = a + b mod p
void bn254fr_addmod(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);

// Subtraction: out = a - b mod p
void bn254fr_submod(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);

// Multiplication: out = a * b mod p
void bn254fr_mulmod(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);

// Division: out = a / b mod p
void bn254fr_divmod(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);

// Inversion: out = a^(-1) mod p
void bn254fr_invmod(bn254fr_t out, const bn254fr_t a);

// Negation: out = -a mod p
void bn254fr_negmod(bn254fr_t out, const bn254fr_t a);

// Exponentiation: out = a^b mod p
void bn254fr_powmod(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);

Constraint Functions

These functions enforce constraints in the zero-knowledge proof:

// Assert a == b
void bn254fr_assert_equal(const bn254fr_t a, const bn254fr_t b);

// Assert out == a + b (linear constraint)
void bn254fr_assert_add(const bn254fr_t out, const bn254fr_t a, const bn254fr_t b);

// Assert out == a * b (quadratic constraint)
void bn254fr_assert_mul(const bn254fr_t out, const bn254fr_t a, const bn254fr_t b);

// Assert out == a * k (multiply by constant)
void bn254fr_assert_mulc(const bn254fr_t out, const bn254fr_t a, const bn254fr_t k);

Checked Operations

These perform the operation and automatically add constraints:

void bn254fr_addmod_checked(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);
void bn254fr_submod_checked(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);
void bn254fr_mulmod_checked(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);
void bn254fr_divmod_checked(bn254fr_t out, const bn254fr_t a, const bn254fr_t b);
void bn254fr_invmod_checked(bn254fr_t out, const bn254fr_t a);

Comparisons

bool bn254fr_eq(const bn254fr_t a, const bn254fr_t b);   // a == b
bool bn254fr_lt(const bn254fr_t a, const bn254fr_t b); // a < b
bool bn254fr_lte(const bn254fr_t a, const bn254fr_t b); // a <= b
bool bn254fr_gt(const bn254fr_t a, const bn254fr_t b); // a > b
bool bn254fr_gte(const bn254fr_t a, const bn254fr_t b); // a >= b
bool bn254fr_eqz(const bn254fr_t a); // a == 0

Bitwise Operations

// Bitwise operations
void bn254fr_band(bn254fr_t out, const bn254fr_t a, const bn254fr_t b); // AND
void bn254fr_bor(bn254fr_t out, const bn254fr_t a, const bn254fr_t b); // OR
void bn254fr_bxor(bn254fr_t out, const bn254fr_t a, const bn254fr_t b); // XOR
void bn254fr_bnot(bn254fr_t out, const bn254fr_t a); // NOT

// Bit decomposition
void bn254fr_to_bits(bn254fr_t* outs, const bn254fr_t a, uint32_t count);
void bn254fr_from_bits(bn254fr_t out, const bn254fr_t* bits, uint32_t count);

// With constraints
void bn254fr_to_bits_checked(const bn254fr_t* out, const bn254fr_t a, uint32_t count);
void bn254fr_from_bits_checked(const bn254fr_t out, const bn254fr_t* bits, uint32_t count);

Utility Functions

// Copy value
void bn254fr_copy(bn254fr_t dest, const bn254fr_t src);

// Get as uint64_t
uint64_t bn254fr_get_u64(const bn254fr_t x);

// Debug print (base 10 or 16)
void bn254fr_print(const bn254fr_t a, uint32_t base = 16);

// Multiplexer: out = cond ? a1 : a0
void bn254fr_mux(bn254fr_t out, const bn254fr_t cond,
const bn254fr_t a0, const bn254fr_t a1);

Rust API

The Rust SDK provides Bn254Fr, which is equivalent to the C++ bn254fr_class wrapper:

use ligetron::bn254fr::*;

// Create field element (automatic memory management)
let mut a = Bn254Fr::new();
a.set_u64(42);

// Arithmetic
let mut sum = Bn254Fr::new();
sum.addmod(&a, &b);

// With constraints
sum.addmod_checked(&a, &b);

// Comparisons
if a.eq(&b) {
// ...
}

// Destructor handles cleanup automatically

Most Rust examples use Bn254Fr for convenience.

Constraint Semantics

bn254fr_t behaves like a normal mutable variable when used for pure computation. However, once you apply any constraint (e.g., via bn254fr_assert_add, assert_equal), the value becomes semantically frozen: it is part of the constraint system.

After constraint assertion:

  • You must treat the value as read-only
  • Reassigning or modifying the handle without freeing it may silently break circuit correctness

⚠️ Constraints are lazy and only enforced upon bn254fr_free(): the system accumulates assertions and processes them on deallocation.

Correct Usage Example

bn254fr_t a, b, sum;
bn254fr_alloc(a);
bn254fr_alloc(b);
bn254fr_alloc(sum);

bn254fr_set_u64(a, 10);
bn254fr_set_u64(b, 20);

bn254fr_addmod(sum, a, b); // sum = a + b mod p
bn254fr_assert_add(sum, a, b); // enforce: sum == a + b

// ❌ INCORRECT: modifies a constrained value
// bn254fr_set_u64(sum, 42); // breaks constraint semantics

// ✅ CORRECT: free before reuse
bn254fr_free(sum);
bn254fr_alloc(sum);
bn254fr_set_u64(sum, 42); // safe

bn254fr_free(a);
bn254fr_free(b);
bn254fr_free(sum);