Skip to content

Commit

Permalink
feat: naive structured execution trace (#5853)
Browse files Browse the repository at this point in the history
Adds logic for executing Honk/PG with a structured execution trace, i.e.
one where each block (gate type) has a static fixed size. (This size is
arbitrarily set to 2^10 for now). This is achieved by simply offsetting
the placement of the gate data into the polynomials based on the fixed
block size in `ExecutionTrace_` rather than densely packing the blocks.

The logic is tested for full proof construction/verification and
folding/deciding for both UH and UGH. This includes a PG test that
demonstrates that the structured trace allows for folding
"inhomogeneous" circuits, i.e. circuits with a differing number of
constraints.

Note: This is not the end of the story since without additional
optimizations this approach would be extremely inefficient in terms of
memory and computation. Some of these optimizations are described in the
corresponding Milestone.

`ClientIVCBench/Full/6      21623 ms        16526 ms            1`
  • Loading branch information
ledwards2225 authored Apr 26, 2024
1 parent c359d79 commit 23aab17
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
namespace bb {

template <class Flavor>
void ExecutionTrace_<Flavor>::populate(Builder& builder, typename Flavor::ProvingKey& proving_key)
void ExecutionTrace_<Flavor>::populate(Builder& builder, typename Flavor::ProvingKey& proving_key, bool is_structured)
{
// Construct wire polynomials, selector polynomials, and copy cycles from raw circuit data
auto trace_data = construct_trace_data(builder, proving_key.circuit_size);
auto trace_data = construct_trace_data(builder, proving_key.circuit_size, is_structured);

add_wires_and_selectors_to_proving_key(trace_data, builder, proving_key);

Expand Down Expand Up @@ -69,7 +69,8 @@ void ExecutionTrace_<Flavor>::add_memory_records_to_proving_key(TraceData& trace

template <class Flavor>
typename ExecutionTrace_<Flavor>::TraceData ExecutionTrace_<Flavor>::construct_trace_data(Builder& builder,
size_t dyadic_circuit_size)
size_t dyadic_circuit_size,
bool is_structured)
{
TraceData trace_data{ dyadic_circuit_size, builder };

Expand Down Expand Up @@ -113,7 +114,12 @@ typename ExecutionTrace_<Flavor>::TraceData ExecutionTrace_<Flavor>::construct_t
trace_data.pub_inputs_offset = offset;
}

offset += block_size;
// If the trace is structured, we populate the data from the next block at a fixed block size offset
if (is_structured) {
offset += builder.FIXED_BLOCK_SIZE;
} else { // otherwise, the next block starts immediately following the previous one
offset += block_size;
}
}
return trace_data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ template <class Flavor> class ExecutionTrace_ {

/**
* @brief Given a circuit, populate a proving key with wire polys, selector polys, and sigma/id polys
* @note By default, this method constructs an exectution trace that is sorted by gate type. Optionally, it
* constructs a trace that is both sorted and "structured" in the sense that each block/gate-type has a fixed amount
* of space within the wire polynomials, regardless of how many actual constraints of each type exist. This is
* useful primarily for folding since it guarantees that the set of relations that must be executed at each row is
* consistent across all instances.
*
* @param builder
* @param is_structured whether or not the trace is to be structured with a fixed block size
*/
static void populate(Builder& builder, ProvingKey&);
static void populate(Builder& builder, ProvingKey&, bool is_structured = false);

private:
/**
Expand Down Expand Up @@ -78,9 +84,10 @@ template <class Flavor> class ExecutionTrace_ {
*
* @param builder
* @param dyadic_circuit_size
* @param is_structured whether or not the trace is to be structured with a fixed block size
* @return TraceData
*/
static TraceData construct_trace_data(Builder& builder, size_t dyadic_circuit_size);
static TraceData construct_trace_data(Builder& builder, size_t dyadic_circuit_size, bool is_structured = false);

/**
* @brief Populate the public inputs block
Expand Down
11 changes: 10 additions & 1 deletion barretenberg/cpp/src/barretenberg/goblin/mock_circuits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class GoblinMockCircuits {
*
* @param builder
*/
static void construct_simple_circuit(GoblinUltraBuilder& builder)
static void add_some_ecc_op_gates(GoblinUltraBuilder& builder)
{
// Add some arbitrary ecc op gates
for (size_t i = 0; i < 3; ++i) {
Expand All @@ -127,7 +127,16 @@ class GoblinMockCircuits {
}
// queues the result of the preceding ECC
builder.queue_ecc_eq(); // should be eq and reset
}

/**
* @brief Generate a simple test circuit with some ECC op gates and conventional arithmetic gates
*
* @param builder
*/
static void construct_simple_circuit(GoblinUltraBuilder& builder)
{
add_some_ecc_op_gates(builder);
MockCircuits::construct_arithmetic_circuit(builder);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ template <typename FF_> class UltraArith {
public:
static constexpr size_t NUM_WIRES = 4;
static constexpr size_t NUM_SELECTORS = 11;
static constexpr size_t FIXED_BLOCK_SIZE = 1 << 10; // Size of each block in a structured trace (arbitrary for now)
using FF = FF_;

class UltraTraceBlock : public ExecutionTraceBlock<FF, NUM_WIRES, NUM_SELECTORS> {
Expand Down Expand Up @@ -165,7 +166,7 @@ template <typename FF_> class UltraArith {

auto get() { return RefArray{ pub_inputs, arithmetic, delta_range, elliptic, aux, lookup }; }

void summarize()
void summarize() const
{
info("Gate blocks summary:");
info("pub inputs:\t", pub_inputs.size());
Expand Down Expand Up @@ -196,6 +197,8 @@ template <typename FF_> class UltraHonkArith {
public:
static constexpr size_t NUM_WIRES = 4;
static constexpr size_t NUM_SELECTORS = 14;
static constexpr size_t FIXED_BLOCK_SIZE = 1 << 10; // Size of each block in a structured trace (arbitrary for now)

using FF = FF_;

class UltraHonkTraceBlock : public ExecutionTraceBlock<FF, NUM_WIRES, NUM_SELECTORS> {
Expand Down Expand Up @@ -279,7 +282,7 @@ template <typename FF_> class UltraHonkArith {
aux, lookup, busread, poseidon_external, poseidon_internal };
}

void summarize()
void summarize() const
{
info("Gate blocks summary:");
info("goblin ecc op:\t", ecc_op.size());
Expand Down
112 changes: 94 additions & 18 deletions barretenberg/cpp/src/barretenberg/protogalaxy/protogalaxy.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "barretenberg/protogalaxy/decider_verifier.hpp"
#include "barretenberg/protogalaxy/protogalaxy_prover.hpp"
#include "barretenberg/protogalaxy/protogalaxy_verifier.hpp"
#include "barretenberg/stdlib_circuit_builders/mock_circuits.hpp"

#include <gtest/gtest.h>

Expand Down Expand Up @@ -43,36 +44,35 @@ template <typename Flavor> class ProtoGalaxyTests : public testing::Test {

static void construct_circuit(Builder& builder)
{
MockCircuits::add_arithmetic_gates(builder);
if constexpr (IsGoblinFlavor<Flavor>) {
GoblinMockCircuits::construct_simple_circuit(builder);
} else {
FF a = FF::random_element();
FF b = FF::random_element();
FF c = FF::random_element();
FF d = a + b + c;
uint32_t a_idx = builder.add_public_variable(a);
uint32_t b_idx = builder.add_variable(b);
uint32_t c_idx = builder.add_variable(c);
uint32_t d_idx = builder.add_variable(d);

builder.create_big_add_gate({ a_idx, b_idx, c_idx, d_idx, FF(1), FF(1), FF(1), FF(-1), FF(0) });
GoblinMockCircuits::add_some_ecc_op_gates(builder);
}
}

// Construct prover and verifier instance for a provided circuit and add to tuple
static void construct_prover_and_verifier_instance(TupleOfInstances& instances,
Builder& builder,
bool structured = false)
{

auto prover_instance = std::make_shared<ProverInstance>(builder, structured);
auto verification_key = std::make_shared<VerificationKey>(prover_instance->proving_key);
auto verifier_instance = std::make_shared<VerifierInstance>(verification_key);
get<0>(instances).emplace_back(prover_instance);
get<1>(instances).emplace_back(verifier_instance);
}

// constructs num_insts number of prover and verifier instances
static TupleOfInstances construct_instances(size_t num_insts)
static TupleOfInstances construct_instances(size_t num_insts, bool structured = false)
{
TupleOfInstances instances;
// TODO(https://github.com/AztecProtocol/barretenberg/issues/938): Parallelize this loop
for (size_t idx = 0; idx < num_insts; idx++) {
auto builder = typename Flavor::CircuitBuilder();
construct_circuit(builder);

auto prover_instance = std::make_shared<ProverInstance>(builder);
auto verification_key = std::make_shared<VerificationKey>(prover_instance->proving_key);
auto verifier_instance = std::make_shared<VerifierInstance>(verification_key);
get<0>(instances).emplace_back(prover_instance);
get<1>(instances).emplace_back(verifier_instance);
construct_prover_and_verifier_instance(instances, builder, structured);
}
return instances;
}
Expand Down Expand Up @@ -332,6 +332,73 @@ template <typename Flavor> class ProtoGalaxyTests : public testing::Test {
decide_and_verify(prover_accumulator_2, verifier_accumulator_2, true);
}

/**
* @brief Testing two valid rounds of folding followed by the decider for a structured trace.
*
*/
static void test_full_protogalaxy_structured_trace()
{
bool structured = true;
TupleOfInstances instances = construct_instances(2, structured);

auto [prover_accumulator, verifier_accumulator] = fold_and_verify(get<0>(instances), get<1>(instances));
check_accumulator_target_sum_manual(prover_accumulator, true);

TupleOfInstances instances_2 = construct_instances(1, structured); // just one set of prover/verifier instances

auto [prover_accumulator_2, verifier_accumulator_2] = fold_and_verify(
{ prover_accumulator, get<0>(instances_2)[0] }, { verifier_accumulator, get<1>(instances_2)[0] });
check_accumulator_target_sum_manual(prover_accumulator_2, true);
info(prover_accumulator_2->proving_key.circuit_size);
decide_and_verify(prover_accumulator_2, verifier_accumulator_2, true);
}

/**
* @brief Testing two valid rounds of folding followed by the decider for a structured trace.
* @details Here we're interested in folding inhomogeneous circuits, i.e. circuits with different numbers of
* constraints, which should be automatically handled by the structured trace
*
*/
static void test_full_protogalaxy_structured_trace_inhomogeneous_circuits()
{
bool structured = true;

// Construct three circuits to be folded, each with a different number of constraints
Builder builder1;
Builder builder2;
Builder builder3;
construct_circuit(builder1);
construct_circuit(builder2);
construct_circuit(builder3);

// Create inhomogenous circuits by adding a different number of add gates to each
MockCircuits::add_arithmetic_gates(builder1, 10);
MockCircuits::add_arithmetic_gates(builder2, 100);
MockCircuits::add_arithmetic_gates(builder3, 1000);

// Construct the Prover/Verifier instances for the first two circuits
TupleOfInstances instances;
construct_prover_and_verifier_instance(instances, builder1, structured);
construct_prover_and_verifier_instance(instances, builder2, structured);

// Fold the first two instances
auto [prover_accumulator, verifier_accumulator] = fold_and_verify(get<0>(instances), get<1>(instances));
check_accumulator_target_sum_manual(prover_accumulator, true);

// Construct the Prover/Verifier instance for the third circuit
TupleOfInstances instances_2;
construct_prover_and_verifier_instance(instances_2, builder3, structured);

// Fold 3rd instance into accumulator
auto [prover_accumulator_2, verifier_accumulator_2] = fold_and_verify(
{ prover_accumulator, get<0>(instances_2)[0] }, { verifier_accumulator, get<1>(instances_2)[0] });
check_accumulator_target_sum_manual(prover_accumulator_2, true);
info(prover_accumulator_2->proving_key.circuit_size);

// Decide on final accumulator
decide_and_verify(prover_accumulator_2, verifier_accumulator_2, true);
}

/**
* @brief Ensure tampering a commitment and then calling the decider causes the decider verification to fail.
*
Expand Down Expand Up @@ -431,6 +498,15 @@ TYPED_TEST(ProtoGalaxyTests, FullProtogalaxyTest)
TestFixture::test_full_protogalaxy();
}

TYPED_TEST(ProtoGalaxyTests, FullProtogalaxyStructuredTrace)
{
TestFixture::test_full_protogalaxy_structured_trace();
}
TYPED_TEST(ProtoGalaxyTests, FullProtogalaxyStructuredTraceInhomogeneous)
{
TestFixture::test_full_protogalaxy_structured_trace_inhomogeneous_circuits();
}

TYPED_TEST(ProtoGalaxyTests, TamperedCommitment)
{
TestFixture::test_tampered_commitment();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,16 @@ class MockCircuits {
using Point = Curve::AffineElement;

/**
* @brief Populate a builder with a specified number of arithmetic gates; includes a PI
* @brief Add a specified number of arithmetic gates (with public inputs) to the provided circuit
*
* @param builder
* @param num_gates
*/
template <typename Builder>
static void construct_arithmetic_circuit(Builder& builder, const size_t target_log2_dyadic_size = 4)
static void add_arithmetic_gates_with_public_inputs(Builder& builder, const size_t num_gates = 4)
{
const size_t target_dyadic_size = 1 << target_log2_dyadic_size;
const size_t num_preamble_gates = builder.num_gates;
ASSERT(target_dyadic_size >= num_preamble_gates);

// For good measure, include a gate with some public inputs
if (target_dyadic_size > num_preamble_gates) {
for (size_t i = 0; i < num_gates; ++i) {
FF a = FF::random_element();
FF b = FF::random_element();
FF c = FF::random_element();
Expand All @@ -35,6 +31,48 @@ class MockCircuits {

builder.create_big_add_gate({ a_idx, b_idx, c_idx, d_idx, FF(1), FF(1), FF(1), FF(-1), FF(0) });
}
}

/**
* @brief Add a specified number of arithmetic gates to the provided circuit
*
* @param builder
* @param num_gates
*/
template <typename Builder> static void add_arithmetic_gates(Builder& builder, const size_t num_gates = 4)
{
// For good measure, include a gate with some public inputs
for (size_t i = 0; i < num_gates; ++i) {
FF a = FF::random_element();
FF b = FF::random_element();
FF c = FF::random_element();
FF d = a + b + c;
uint32_t a_idx = builder.add_variable(a);
uint32_t b_idx = builder.add_variable(b);
uint32_t c_idx = builder.add_variable(c);
uint32_t d_idx = builder.add_variable(d);

builder.create_big_add_gate({ a_idx, b_idx, c_idx, d_idx, FF(1), FF(1), FF(1), FF(-1), FF(0) });
}
}

/**
* @brief Populate a builder with a specified number of arithmetic gates; includes a PI
*
* @param builder
* @param num_gates
*/
template <typename Builder>
static void construct_arithmetic_circuit(Builder& builder, const size_t target_log2_dyadic_size = 4)
{
const size_t target_dyadic_size = 1 << target_log2_dyadic_size;
const size_t num_preamble_gates = builder.num_gates;
ASSERT(target_dyadic_size >= num_preamble_gates);

// For good measure, include a gate with some public inputs
if (target_dyadic_size > num_preamble_gates) {
add_arithmetic_gates_with_public_inputs(builder, 1);
}

// A proper treatment of this would dynamically calculate how many gates to add given static information about
// Builder, but a major overhaul of the execution trace is underway, so we just elect to use a hack. Namely, for
Expand All @@ -46,19 +84,10 @@ class MockCircuits {

// to prevent underflow of the loop upper limit; target size >= 16 should suffice
ASSERT(target_dyadic_size > OFFSET_HACK + num_preamble_gates);
// Add arbitrary arithmetic gates to obtain a total of num_gates-many gates
FF a = FF::random_element();
FF b = FF::random_element();
FF c = FF::random_element();
FF d = a + b + c;
uint32_t a_idx = builder.add_variable(a);
uint32_t b_idx = builder.add_variable(b);
uint32_t c_idx = builder.add_variable(c);
uint32_t d_idx = builder.add_variable(d);
size_t num_gates_to_add = target_dyadic_size - OFFSET_HACK - 1 - num_preamble_gates;

for (size_t i = 0; i < target_dyadic_size - OFFSET_HACK - 1 - num_preamble_gates; ++i) {
builder.create_big_add_gate({ a_idx, b_idx, c_idx, d_idx, FF(1), FF(1), FF(1), FF(-1), FF(0) });
}
// Add arbitrary arithmetic gates to obtain a total of num_gates-many gates
add_arithmetic_gates(builder, num_gates_to_add);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ template <typename FF> class StandardCircuitBuilder_ : public CircuitBuilderBase
using Arithmetization = StandardArith<FF>;
using GateBlocks = typename Arithmetization::TraceBlocks;
static constexpr size_t NUM_WIRES = Arithmetization::NUM_WIRES;
static constexpr size_t FIXED_BLOCK_SIZE = 0; // not used, for compatibility only
// Keeping NUM_WIRES, at least temporarily, for backward compatibility
static constexpr size_t program_width = Arithmetization::NUM_WIRES;
static constexpr size_t num_selectors = Arithmetization::NUM_SELECTORS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class UltraCircuitBuilder_ : public CircuitBuilderBase<typename Arithmetization_

using FF = typename Arithmetization::FF;
static constexpr size_t NUM_WIRES = Arithmetization::NUM_WIRES;
static constexpr size_t FIXED_BLOCK_SIZE = Arithmetization::FIXED_BLOCK_SIZE;
// Keeping NUM_WIRES, at least temporarily, for backward compatibility
static constexpr size_t program_width = Arithmetization::NUM_WIRES;
static constexpr size_t num_selectors = Arithmetization::NUM_SELECTORS;
Expand Down
Loading

0 comments on commit 23aab17

Please sign in to comment.