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:
- C API (
bn254fr_t) - Low-level C interface with explicit memory management - C++ Wrapper (
bn254fr_class) - Convenient C++ class with automatic memory management and operator overloading - 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);