Skip to content

Commit

Permalink
feat: stdlib databus (#5598)
Browse files Browse the repository at this point in the history
Introduces a `stdlib::DataBus` class to facilitate calldata/return_data
reads/writes. The work also includes:

- Circuit checker functionality for checking the validity of databus
read gates (simply checks whether the {idx, value} pair in the gate
corresponds to data in the corresponding bus vector). Closes
AztecProtocol/barretenberg#842
- Tests for the stdlib DataBus (including failure tests and
demonstration of check circuit)
- Minor updates to the builder notion of DataBus; now an array of
`BusVector`s to improve interface from stdlib
  • Loading branch information
ledwards2225 committed Apr 9, 2024
1 parent b02d1e1 commit 633a711
Show file tree
Hide file tree
Showing 10 changed files with 352 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ bool UltraCircuitChecker::check_block(Builder& builder,
info("Failed PoseidonExternal relation at row idx = ", idx);
return false;
}
result = result && check_databus_read(values, builder);
if (result == false) {
info("Failed databus read at row idx = ", idx);
return false;
}
}
if (result == false) {
info("Failed at row idx = ", idx);
Expand Down Expand Up @@ -156,6 +161,33 @@ bool UltraCircuitChecker::check_lookup(auto& values, auto& lookup_hash_table)
return true;
};

template <typename Builder> bool UltraCircuitChecker::check_databus_read(auto& values, Builder& builder)
{
if (!values.q_busread.is_zero()) {
// Extract the {index, value} pair from the read gate inputs
auto raw_read_idx = static_cast<size_t>(uint256_t(values.w_r));
auto value = values.w_l;

// Determine the type of read based on selector values
bool is_calldata_read = (values.q_l == 1);
bool is_return_data_read = (values.q_r == 1);
ASSERT(is_calldata_read || is_return_data_read);

// Check that the claimed value is present in the calldata/return data at the corresponding index
FF bus_value;
if (is_calldata_read) {
auto calldata = builder.get_calldata();
bus_value = builder.get_variable(calldata[raw_read_idx]);
}
if (is_return_data_read) {
auto return_data = builder.get_return_data();
bus_value = builder.get_variable(return_data[raw_read_idx]);
}
return (value == bus_value);
}
return true;
};

bool UltraCircuitChecker::check_tag_data(const TagCheckData& tag_data)
{
return tag_data.left_product == tag_data.right_product;
Expand Down Expand Up @@ -254,6 +286,7 @@ void UltraCircuitChecker::populate_values(
values.q_aux = block.q_aux()[idx];
values.q_lookup = block.q_lookup_type()[idx];
if constexpr (IsGoblinBuilder<Builder>) {
values.q_busread = block.q_busread()[idx];
values.q_poseidon2_internal = block.q_poseidon2_internal()[idx];
values.q_poseidon2_external = block.q_poseidon2_external()[idx];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ class UltraCircuitChecker {
*/
static bool check_lookup(auto& values, auto& lookup_hash_table);

/**
* @brief Check that the {index, value} pair contained in a databus read gate reflects the actual value present in
* the corresponding databus column at the given index
*
* @param values Inputs to a databus read gate
*/
template <typename Builder> static bool check_databus_read(auto& values, Builder& builder);

/**
* @brief Check whether the left and right running tag products are equal
* @note By construction, this is in general only true after the last gate has been processed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#include "databus.hpp"
#include "../circuit_builders/circuit_builders.hpp"

namespace bb::stdlib {

template <typename Builder> void databus<Builder>::bus_vector::set_values(const std::vector<field_pt>& entries_in)
{
// Set the context from the input entries
for (const auto& entry : entries_in) {
if (entry.get_context() != nullptr) {
context = entry.get_context();
break;
}
}
// Enforce that builder context is known at this stage. Otherwise first read will fail if the index is a constant.
ASSERT(context != nullptr);

// Initialize the bus vector entries from the input entries which are un-normalized and possibly constants
for (const auto& entry : entries_in) {
if (entry.is_constant()) { // create a constant witness from the constant
auto const_var_idx = context->put_constant_variable(entry.get_value());
entries.emplace_back(field_pt::from_witness_index(context, const_var_idx));
} else { // normalize the raw entry
entries.emplace_back(entry.normalize());
}
// Add the entry to the bus vector data
context->append_to_bus_vector(bus_idx, entries.back().get_witness_index());
}
length = entries.size();
}

template <typename Builder> field_t<Builder> databus<Builder>::bus_vector::operator[](const field_pt& index) const
{
// Ensure the read is valid
auto raw_index = static_cast<size_t>(uint256_t(index.get_value()).data[0]);
if (raw_index >= length) {
context->failure("bus_vector: access out of bounds");
}

// The read index must be a witness; if constant, add it as a constant variable
uint32_t index_witness_idx = 0;
if (index.is_constant()) {
index_witness_idx = context->put_constant_variable(index.get_value());
} else {
index_witness_idx = index.normalize().get_witness_index();
}

// Read from the bus vector at the specified index. Creates a single read gate
uint32_t output_idx = context->read_bus_vector(bus_idx, index_witness_idx);
return field_pt::from_witness_index(context, output_idx);
}

template class databus<bb::GoblinUltraCircuitBuilder>;
} // namespace bb::stdlib
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once
#include "../circuit_builders/circuit_builders_fwd.hpp"
#include "../field/field.hpp"
#include "barretenberg/stdlib_circuit_builders/databus.hpp"

namespace bb::stdlib {

template <typename Builder> class databus {
public:
databus() = default;

private:
class bus_vector {
private:
using field_pt = field_t<Builder>;

public:
bus_vector(const BusId bus_idx)
: bus_idx(bus_idx){};

/**
* @brief Set the entries of the bus vector from possibly unnormalized or constant inputs
* @note A builder/context is assumed to be known at this stage, otherwise the first read will fail if index is
* constant
*
* @tparam Builder
* @param entries_in
*/
void set_values(const std::vector<field_pt>& entries_in);

/**
* @brief Read from the bus vector with a witness index value. Creates a read gate
*
* @param index
* @return field_pt
*/
field_pt operator[](const field_pt& index) const;

size_t size() const { return length; }
Builder* get_context() const { return context; }

private:
mutable std::vector<field_pt> entries; // bus vector entries
size_t length = 0;
BusId bus_idx; // Idx of column in bus
mutable Builder* context = nullptr;
};

public:
// The columns of the DataBus
bus_vector calldata{ BusId::CALLDATA };
bus_vector return_data{ BusId::RETURNDATA };
};
} // namespace bb::stdlib
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@

#include <gtest/gtest.h>

#include "barretenberg/circuit_checker/circuit_checker.hpp"
#include "barretenberg/numeric/random/engine.hpp"
#include "barretenberg/stdlib_circuit_builders/goblin_ultra_circuit_builder.hpp"
#include "databus.hpp"

using Builder = GoblinUltraCircuitBuilder;
using field_ct = stdlib::field_t<Builder>;
using witness_ct = stdlib::witness_t<Builder>;
using databus_ct = stdlib::databus<Builder>;

namespace {
auto& engine = numeric::get_debug_randomness();
}

/**
* @brief An expository test demonstrating the functionality of the databus in a small but representative use case
*
*/
TEST(Databus, CallDataAndReturnData)
{
Builder builder;
databus_ct databus;

// The databus is advantageous in situations where we want to pass large amounts of public inputs between circuits
// in a chain (like private function execution in Aztec) but where we only need to use a small subset of those
// values in any given circuit. As an example of this utility, consider the case where the output (return data) is
// defined by simply taking the last two elements of the input (calldata) and summing them together. We can use the
// databus mechanism to establish that the return data was indeed formed in this way.

// Define some bus data that conform to the pattern described above
std::array<bb::fr, 4> raw_calldata_values = { 4, 5, 6, 7 };
std::array<bb::fr, 3> raw_return_data_values = { 4, 5, 13 }; // 13 = 6 + 7

// Populate the calldata in the databus
std::vector<field_ct> calldata_values;
for (auto& value : raw_calldata_values) {
calldata_values.emplace_back(witness_ct(&builder, value));
}
databus.calldata.set_values(calldata_values);

// Populate the return data in the databus
std::vector<field_ct> return_data_values;
for (auto& value : raw_return_data_values) {
return_data_values.emplace_back(witness_ct(&builder, value));
}
databus.return_data.set_values(return_data_values);

// Establish that the first two outputs are simply copied over from the inputs. Each 'copy' requires two read gates.
field_ct idx_0(witness_ct(&builder, 0));
field_ct idx_1(witness_ct(&builder, 1));
databus.calldata[idx_0].assert_equal(databus.return_data[idx_0]);
databus.calldata[idx_1].assert_equal(databus.return_data[idx_1]);

// Get the last two entries in calldata and compute their sum
field_ct idx_2(witness_ct(&builder, 2));
field_ct idx_3(witness_ct(&builder, 3));
// This line creates an arithmetic gate and two calldata read gates (via operator[]).
field_ct sum = databus.calldata[idx_2] + databus.calldata[idx_3];

// Read the last index of the return data. (Creates a return data read gate via operator[]).
field_ct idx(witness_ct(&builder, 2));
field_ct read_result = databus.return_data[idx];

// By construction, the last return data value is equal to the sum of the last two calldata values
EXPECT_EQ(sum.get_value(), read_result.get_value());

// Asserting that 'sum' is equal to the read result completes the process of establishing that the corresponding
// return data entry was formed correctly; 'sum' is equal to the read result (enforced via copy constraint) and the
// read result is connected to the value in the databus return data column via the read gate. 'sum' is connected to
// the calldata values via an arithmetic gate and the two calldata read gates.
sum.assert_equal(read_result);

EXPECT_TRUE(CircuitChecker::check(builder));
}

/**
* @brief A failure test demonstrating that trying to prove (via a databus read) that an erroneous value is present in
* the databus will result in an invalid witness.
*
*/
TEST(Databus, BadReadFailure)
{
Builder builder;
databus_ct databus;

// Populate return data with a single arbitrary value
bb::fr actual_value = 13;
databus.return_data.set_values({ witness_ct(&builder, actual_value) });

// Read the value from the return data
size_t raw_idx = 0; // read at 0th index
field_ct idx(witness_ct(&builder, raw_idx));
field_ct read_result = databus.return_data[idx];

// The result of the read should be as expected
EXPECT_EQ(actual_value, read_result.get_value());

// Since the read gate implicitly created by using operator[] on return data is valid, the witness is valid
EXPECT_TRUE(CircuitChecker::check(builder));

// Now assert that the read result is equal to some erroneous value. This effectively updates the return data read
// gate to attest to the erroneous value being present at index 0 in the return data.
field_ct erroneous_value(witness_ct(&builder, actual_value - 1));
erroneous_value.assert_equal(read_result);

// Since the read gate is no longer valid, the circuit checker will fail
EXPECT_FALSE(CircuitChecker::check(builder));
}

/**
* @brief A failure test demonstrating that a bad input-output 'copy' will lead to an invalid witness
*
*/
TEST(Databus, BadCopyFailure)
{
Builder builder;
databus_ct databus;

// Populate calldata with a single input
bb::fr input = 13;
databus.calldata.set_values({ witness_ct(&builder, input) });

// Populate return data with an output different from the input
bb::fr output = input - 1;
databus.return_data.set_values({ witness_ct(&builder, output) });

// Attempt to attest that the calldata has been copied into the return data
size_t raw_idx = 0; // read at 0th index
field_ct idx(witness_ct(&builder, raw_idx));
databus.calldata[idx].assert_equal(databus.return_data[idx]);

// Since the output data is not a copy of the input, the checker should fail
EXPECT_FALSE(CircuitChecker::check(builder));
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ struct BusVector {
* in-circuit as we would with public inputs).
*
*/
struct DataBus {
BusVector calldata; // the public input to the circuit
BusVector return_data; // the public output of the circuit
};
using DataBus = std::array<BusVector, 2>;
enum class BusId { CALLDATA, RETURNDATA };

} // namespace bb
Loading

0 comments on commit 633a711

Please sign in to comment.