Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,54 @@ std::array<field_t<Builder>, 3> field_t<Builder>::slice(const uint8_t msb, const
return result;
}

template <typename Builder>
std::pair<field_t<Builder>, field_t<Builder>> field_t<Builder>::split_at(const size_t lsb_index,
const size_t num_bits) const
{
ASSERT(lsb_index < num_bits);
ASSERT(num_bits <= grumpkin::MAX_NO_WRAP_INTEGER_BIT_LENGTH);

const uint256_t value = get_value();
const uint256_t hi = value >> lsb_index;
const uint256_t lo = value % (uint256_t(1) << lsb_index);

if (is_constant()) {
// If `*this` is constant, we can return the split values directly
ASSERT(lo + (hi << lsb_index) == value);
return std::make_pair(field_t<Builder>(lo), field_t<Builder>(hi));
}

// Handle edge case when lsb_index == 0
if (lsb_index == 0) {
ASSERT(hi == value);
ASSERT(lo == 0);
create_range_constraint(num_bits, "split_at: hi value too large.");
return std::make_pair(field_t<Builder>(0), *this);
}

Builder* ctx = get_context();
ASSERT(ctx != nullptr);

field_t<Builder> lo_wit(witness_t(ctx, lo));
field_t<Builder> hi_wit(witness_t(ctx, hi));

// Ensure that `lo_wit` is in the range [0, 2^lsb_index - 1]
lo_wit.create_range_constraint(lsb_index, "split_at: lo value too large.");

// Ensure that `hi_wit` is in the range [0, 2^(num_bits - lsb_index) - 1]
hi_wit.create_range_constraint(num_bits - lsb_index, "split_at: hi value too large.");

// Check that *this = lo_wit + hi_wit * 2^{lsb_index}
const field_t<Builder> reconstructed = lo_wit + (hi_wit * field_t<Builder>(uint256_t(1) << lsb_index));
assert_equal(reconstructed, "split_at: decomposition failed");

// Set the origin tag for both witnesses
lo_wit.set_origin_tag(tag);
hi_wit.set_origin_tag(tag);

return std::make_pair(lo_wit, hi_wit);
}

/**
* @brief Build constraints establishing the decomposition of `*this` into bits.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ template <typename Builder> class field_t {

std::array<field_t, 3> slice(uint8_t msb, uint8_t lsb) const;

std::pair<field_t<Builder>, field_t<Builder>> split_at(
const size_t lsb_index, const size_t num_bits = grumpkin::MAX_NO_WRAP_INTEGER_BIT_LENGTH) const;

bool_t<Builder> is_zero() const;

void create_range_constraint(size_t num_bits, std::string const& msg = "field_t::range_constraint") const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "barretenberg/circuit_checker/circuit_checker.hpp"
#include "barretenberg/common/streams.hpp"
#include "barretenberg/numeric/random/engine.hpp"
#include "barretenberg/numeric/uint256/uint256.hpp"
#include "barretenberg/stdlib/primitives/circuit_builders/circuit_builders.hpp"
#include <gtest/gtest.h>
#include <utility>
Expand Down Expand Up @@ -859,6 +860,56 @@ template <typename Builder> class stdlib_field : public testing::Test {
EXPECT_TRUE(builder.err() == "slice: hi value too large.");
}

static void test_split_at()
{
Builder builder = Builder();

// Test different bit sizes
std::vector<size_t> test_bit_sizes = { 8, 16, 32, 100, 252 };

// Lambda to check split_at functionality
auto check_split_at = [&](const field_ct& a, size_t start, size_t num_bits) {
const uint256_t a_native = a.get_value();
auto split_data = a.split_at(start, num_bits);
EXPECT_EQ(split_data.first.get_value(), a_native & ((uint256_t(1) << start) - 1));
EXPECT_EQ(split_data.second.get_value(), (a_native >> start) & ((uint256_t(1) << num_bits) - 1));

if (a.is_constant()) {
EXPECT_TRUE(split_data.first.is_constant());
EXPECT_TRUE(split_data.second.is_constant());
}

if (start == 0) {
EXPECT_TRUE(split_data.first.is_constant());
EXPECT_TRUE(split_data.first.get_value() == 0);
EXPECT_EQ(split_data.second.get_value(), a.get_value());
}
};

for (size_t num_bits : test_bit_sizes) {
uint256_t a_native = engine.get_random_uint256() & ((uint256_t(1) << num_bits) - 1);

// check split_at for a constant
field_ct a_constant(a_native);
check_split_at(a_constant, 0, num_bits);
check_split_at(a_constant, num_bits / 4, num_bits);
check_split_at(a_constant, num_bits / 3, num_bits);
check_split_at(a_constant, num_bits / 2, num_bits);
check_split_at(a_constant, num_bits - 1, num_bits);

// check split_at for a witness
field_ct a_witness(witness_ct(&builder, a_native));
check_split_at(a_witness, 0, num_bits);
check_split_at(a_witness, num_bits / 4, num_bits);
check_split_at(a_witness, num_bits / 3, num_bits);
check_split_at(a_witness, num_bits / 2, num_bits);
check_split_at(a_witness, num_bits - 1, num_bits);
}

bool result = CircuitChecker::check(builder);
EXPECT_EQ(result, true);
}

static void test_three_bit_table()
{
Builder builder = Builder();
Expand Down Expand Up @@ -1258,7 +1309,10 @@ template <typename Builder> class stdlib_field : public testing::Test {
static void test_origin_tag_consistency()
{
Builder builder = Builder();
auto a = field_ct(witness_ct(&builder, bb::fr::random_element()));
// Randomly generate a and b (a must ≤ 252 bits)
uint256_t a_val =
uint256_t(bb::fr::random_element()) & ((uint256_t(1) << grumpkin::MAX_NO_WRAP_INTEGER_BIT_LENGTH) - 1);
auto a = field_ct(witness_ct(&builder, a_val));
auto b = field_ct(witness_ct(&builder, bb::fr::random_element()));
EXPECT_TRUE(a.get_origin_tag().is_empty());
EXPECT_TRUE(b.get_origin_tag().is_empty());
Expand Down Expand Up @@ -1362,6 +1416,12 @@ template <typename Builder> class stdlib_field : public testing::Test {
EXPECT_EQ(element.get_origin_tag(), submitted_value_origin_tag);
}

// Split preserves tags
const size_t num_bits = uint256_t(a.get_value()).get_msb() + 1;
auto split_data = a.split_at(num_bits / 2, num_bits);
EXPECT_EQ(split_data.first.get_origin_tag(), submitted_value_origin_tag);
EXPECT_EQ(split_data.second.get_origin_tag(), submitted_value_origin_tag);

// Decomposition preserves tags

auto decomposed_bits = a.decompose_into_bits();
Expand Down Expand Up @@ -1555,6 +1615,10 @@ TYPED_TEST(stdlib_field, test_slice_random)
{
TestFixture::test_slice_random();
}
TYPED_TEST(stdlib_field, test_split_at)
{
TestFixture::test_split_at();
}
TYPED_TEST(stdlib_field, test_three_bit_table)
{
TestFixture::test_three_bit_table();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// === AUDIT STATUS ===
// internal: { status: not started, auditors: [], date: YYYY-MM-DD }
// internal: { status: done, auditors: [suyash], date: 2025-07-23 }
// external_1: { status: not started, auditors: [], date: YYYY-MM-DD }
// external_2: { status: not started, auditors: [], date: YYYY-MM-DD }
// =====================
Expand All @@ -18,13 +18,21 @@ uint<Builder, Native> uint<Builder, Native>::operator+(const uint& other) const

ASSERT(context == other.context || (context != nullptr && other.context == nullptr) ||
(context == nullptr && other.context != nullptr));

Builder* ctx = (context == nullptr) ? other.context : context;

if (is_constant() && other.is_constant()) {
return uint<Builder, Native>(context, (additive_constant + other.additive_constant) & MASK);
}

// N.B. We assume that additive_constant is nonzero ONLY if value is constant
if (!is_constant()) {
BB_ASSERT_EQ(additive_constant, 0ULL, "uint::operator+ expected additive_constant to be zero");
}
if (!other.is_constant()) {
BB_ASSERT_EQ(other.additive_constant, 0ULL, "uint::operator+ expected other.additive_constant to be zero");
}

const uint256_t lhs = get_value();
const uint256_t rhs = other.get_value();
const uint256_t constants = (additive_constant + other.additive_constant) & MASK;
Expand All @@ -48,7 +56,7 @@ uint<Builder, Native> uint<Builder, Native>::operator+(const uint& other) const

uint<Builder, Native> result(ctx);
result.witness_index = gate.c;
result.witness_status = WitnessStatus::WEAK_NORMALIZED;
result.normalize();

return result;
}
Expand All @@ -67,12 +75,19 @@ uint<Builder, Native> uint<Builder, Native>::operator-(const uint& other) const
}

// N.B. We assume that additive_constant is nonzero ONLY if value is constant
if (!is_constant()) {
BB_ASSERT_EQ(additive_constant, 0ULL, "uint::operator- expected additive_constant to be zero");
}
if (!other.is_constant()) {
BB_ASSERT_EQ(other.additive_constant, 0ULL, "uint::operator- expected other.additive_constant to be zero");
}

const uint32_t lhs_idx = is_constant() ? ctx->zero_idx : witness_index;
const uint32_t rhs_idx = other.is_constant() ? ctx->zero_idx : other.witness_index;

const uint256_t lhs = get_value();
const uint256_t rhs = other.get_value();
const uint256_t constant_term = (additive_constant - other.additive_constant);
const uint256_t constant_term = CIRCUIT_UINT_MAX_PLUS_ONE + (additive_constant - other.additive_constant);

const uint256_t difference = CIRCUIT_UINT_MAX_PLUS_ONE + lhs - rhs;
const uint256_t overflow = difference >> width;
Expand All @@ -87,21 +102,24 @@ uint<Builder, Native> uint<Builder, Native>::operator-(const uint& other) const
FF::neg_one(),
FF::neg_one(),
-FF(CIRCUIT_UINT_MAX_PLUS_ONE),
CIRCUIT_UINT_MAX_PLUS_ONE + constant_term,
constant_term,
};

ctx->create_balanced_add_gate(gate);

uint<Builder, Native> result(ctx);
result.witness_index = gate.c;
result.witness_status = WitnessStatus::WEAK_NORMALIZED;
result.normalize();

return result;
}

template <typename Builder, typename Native>
uint<Builder, Native> uint<Builder, Native>::operator*(const uint& other) const
{
ASSERT(context == other.context || (context != nullptr && other.context == nullptr) ||
(context == nullptr && other.context != nullptr));

Builder* ctx = (context == nullptr) ? other.context : context;

if (is_constant() && other.is_constant()) {
Expand All @@ -111,6 +129,14 @@ uint<Builder, Native> uint<Builder, Native>::operator*(const uint& other) const
return other * (*this);
}

// N.B. We assume that additive_constant is nonzero ONLY if value is constant
if (!is_constant()) {
BB_ASSERT_EQ(additive_constant, 0ULL, "uint::operator* expected additive_constant to be zero");
}
if (!other.is_constant()) {
BB_ASSERT_EQ(other.additive_constant, 0ULL, "uint::operator* expected other.additive_constant to be zero");
}

const uint32_t rhs_idx = other.is_constant() ? ctx->zero_idx : other.witness_index;

const uint256_t lhs = ctx->get_variable(witness_index);
Expand Down Expand Up @@ -139,9 +165,8 @@ uint<Builder, Native> uint<Builder, Native>::operator*(const uint& other) const
ctx->decompose_into_default_range(gate.d, width);

uint<Builder, Native> result(ctx);
result.accumulators = constrain_accumulators(ctx, gate.c);
result.witness_index = gate.c;
result.witness_status = WitnessStatus::OK;
result.normalize();

return result;
}
Expand Down Expand Up @@ -184,20 +209,17 @@ std::pair<uint<Builder, Native>, uint<Builder, Native>> uint<Builder, Native>::d
Builder* ctx = (context == nullptr) ? other.context : context;

// We want to force the divisor to be non-zero, as this is an error state
if (other.is_constant() && other.get_value() == 0) {
// TODO: should have an actual error handler!
const uint32_t one = ctx->add_variable(FF::one());
ctx->assert_equal_constant(one, FF::zero(), "plookup_arithmetic: divide by zero!");
} else if (!other.is_constant()) {
const bool_t<Builder> is_divisor_zero = field_t<Builder>(other).is_zero();
ctx->assert_equal_constant(is_divisor_zero.witness_index, FF::zero(), "plookup_arithmetic: divide by zero!");
}
static_cast<field_t<Builder>>(other).assert_is_not_zero("uint_arithmetic: divide by zero!");

// If both are constants, we can compute the quotient and remainder directly
if (is_constant() && other.is_constant()) {
const uint<Builder, Native> remainder(ctx, additive_constant % other.additive_constant);
const uint<Builder, Native> quotient(ctx, additive_constant / other.additive_constant);
return std::make_pair(quotient, remainder);
} else if (witness_index == other.witness_index) {
}

// If the divisor and dividend are the same witness, we can return a quotient of 1 and a remainder of 0.
if (witness_index == other.witness_index) {
const uint<Builder, Native> remainder(context, 0);
const uint<Builder, Native> quotient(context, 1);
return std::make_pair(quotient, remainder);
Expand Down Expand Up @@ -248,13 +270,11 @@ std::pair<uint<Builder, Native>, uint<Builder, Native>> uint<Builder, Native>::d
ctx->decompose_into_default_range(delta_idx, width);
uint<Builder, Native> quotient(ctx);
quotient.witness_index = quotient_idx;
quotient.accumulators = constrain_accumulators(ctx, quotient.witness_index);
quotient.witness_status = WitnessStatus::OK;
quotient.normalize();

uint<Builder, Native> remainder(ctx);
remainder.witness_index = remainder_idx;
remainder.accumulators = constrain_accumulators(ctx, remainder.witness_index);
remainder.witness_status = WitnessStatus::OK;
remainder.normalize();

return std::make_pair(quotient, remainder);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// === AUDIT STATUS ===
// internal: { status: not started, auditors: [], date: YYYY-MM-DD }
// internal: { status: done, auditors: [suyash], date: 2025-07-23 }
// external_1: { status: not started, auditors: [], date: YYYY-MM-DD }
// external_2: { status: not started, auditors: [], date: YYYY-MM-DD }
// =====================
Expand Down
Loading
Loading