Skip to content

Commit

Permalink
feat(avm): bytecode avm control flow (#4253)
Browse files Browse the repository at this point in the history
Resolves #4209
  • Loading branch information
jeanmon authored Jan 29, 2024
1 parent 1c8ea49 commit fb1d742
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,15 @@ std::vector<Row> Execution::gen_trace(std::vector<Instruction> const& instructio
{
AvmMiniTraceBuilder trace_builder{};

for (auto const& inst : instructions) {
// copied version of pc maintained in trace builder. The value of pc is evolving based
// on opcode logic and therefore is not maintained here. However, the next opcode in the execution
// is determined by this value which require read access to the code below.
uint32_t pc = 0;
auto const inst_size = instructions.size();

while ((pc = trace_builder.getPc()) < inst_size) {
auto inst = instructions.at(pc);

switch (inst.op_code) {
case OpCode::ADD:
trace_builder.add(inst.operands.at(0), inst.operands.at(1), inst.operands.at(2), inst.in_tag);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ std::vector<FF> AvmMiniTraceBuilder::return_op(uint32_t ret_offset, uint32_t ret
pos = ret_size;
}
}
pc = UINT32_MAX; // This ensures that no subsequent opcode will be executed.
return returnMem;
}

Expand All @@ -471,6 +472,8 @@ void AvmMiniTraceBuilder::halt()
.avmMini_internal_return_ptr = FF(internal_return_ptr),
.avmMini_sel_halt = FF(1),
});

pc = UINT32_MAX; // This ensures that no subsequent opcode will be executed.
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class AvmMiniTraceBuilder {
std::vector<Row> finalize();
void reset();

uint32_t getPc() const { return pc; }

// Addition with direct memory access.
void add(uint32_t a_offset, uint32_t b_offset, uint32_t dst_offset, AvmMemoryTag in_tag);

Expand Down
259 changes: 230 additions & 29 deletions barretenberg/cpp/src/barretenberg/vm/tests/AvmMini_execution.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@
#include <string>
#include <utility>

namespace {
void gen_proof_and_validate(std::vector<uint8_t> const& bytecode,
std::vector<Row>&& trace,
std::vector<FF> const& calldata)
{
auto circuit_builder = AvmMiniCircuitBuilder();
circuit_builder.set_trace(std::move(trace));
EXPECT_TRUE(circuit_builder.check_circuit());

auto composer = honk::AvmMiniComposer();
auto verifier = composer.create_verifier(circuit_builder);

auto proof = avm_trace::Execution::run_and_prove(bytecode, calldata);

EXPECT_TRUE(verifier.verify_proof(proof));
}
} // namespace

namespace tests_avm {
using namespace avm_trace;
using bb::utils::hex_to_bytes;
Expand Down Expand Up @@ -61,17 +79,8 @@ TEST_F(AvmMiniExecutionTests, basicAddReturn)
EXPECT_EQ(instructions.at(1).operands.at(0), 0);

auto trace = Execution::gen_trace(instructions, std::vector<FF>{});
auto trace_verif = trace;
validate_trace_proof(std::move(trace));

auto circuit_builder = AvmMiniCircuitBuilder();
circuit_builder.set_trace(std::move(trace_verif));
auto composer = honk::AvmMiniComposer();
auto verifier = composer.create_verifier(circuit_builder);

auto proof = Execution::run_and_prove(bytecode, std::vector<FF>{});

EXPECT_TRUE(verifier.verify_proof(proof));
gen_proof_and_validate(bytecode, std::move(trace), std::vector<FF>{});
}

// Positive test for SET and SUB opcodes
Expand Down Expand Up @@ -126,17 +135,7 @@ TEST_F(AvmMiniExecutionTests, setAndSubOpcodes)
auto row = std::ranges::find_if(trace.begin(), trace.end(), [](Row r) { return r.avmMini_sel_op_sub == 1; });
EXPECT_EQ(row->avmMini_ic, 10000); // 47123 - 37123 = 10000

auto trace_verif = trace;
validate_trace_proof(std::move(trace));

auto circuit_builder = AvmMiniCircuitBuilder();
circuit_builder.set_trace(std::move(trace_verif));
auto composer = honk::AvmMiniComposer();
auto verifier = composer.create_verifier(circuit_builder);

auto proof = Execution::run_and_prove(bytecode, std::vector<FF>{});

EXPECT_TRUE(verifier.verify_proof(proof));
gen_proof_and_validate(bytecode, std::move(trace), std::vector<FF>{});
}

// Positive test for multiple MUL opcodes
Expand Down Expand Up @@ -209,17 +208,219 @@ TEST_F(AvmMiniExecutionTests, powerWithMulOpcodes)
trace.begin(), trace.end(), [](Row r) { return r.avmMini_sel_op_mul == 1 && r.avmMini_pc == 13; });
EXPECT_EQ(row->avmMini_ic, 244140625); // 5^12 = 244140625

auto trace_verif = trace;
validate_trace_proof(std::move(trace));
gen_proof_and_validate(bytecode, std::move(trace), std::vector<FF>{});
}

auto circuit_builder = AvmMiniCircuitBuilder();
circuit_builder.set_trace(std::move(trace_verif));
auto composer = honk::AvmMiniComposer();
auto verifier = composer.create_verifier(circuit_builder);
// Positive test about a single internal_call and internal_return
// Code of internal routine is SET U32 value 123456789 at memory address 7
// The bytecode execution is:
// SET U32 val. 222111000 at memory address 4
// CALL internal routine
// ADD M[4] with M[7] and output in M[9]
// Internal routine bytecode is at the end.
// Bytecode layout: SET INTERNAL_CALL ADD RETURN SET INTERNAL_RETURN
// 0 1 2 3 4 5
TEST_F(AvmMiniExecutionTests, simpleInternalCall)
{
std::string bytecode_hex = "27" // SET 39 = 0x27
"03" // U32
"0D3D2518" // val 222111000 = 0xD3D2518
"00000004" // dst_offset 4
"25" // INTERNALCALL 37
"00000004" // jmp_dest
"00" // ADD
"03" // U32
"00000004" // addr a 4
"00000007" // addr b 7
"00000009" // addr c9
"34" // RETURN
"00000000" // ret offset 0
"00000000" // ret size 0
"27" // SET 39 = 0x27
"03" // U32
"075BCD15" // val 123456789 = 0x75BCD15
"00000007" // dst_offset 7
"26" // INTERNALRETURN 38
;

auto proof = Execution::run_and_prove(bytecode, std::vector<FF>{});
auto bytecode = hex_to_bytes(bytecode_hex);
auto instructions = Execution::parse(bytecode);

EXPECT_TRUE(verifier.verify_proof(proof));
EXPECT_EQ(instructions.size(), 6);

// We test parsing step for INTERNALCALL and INTERNALRETURN.

// INTERNALCALL
EXPECT_EQ(instructions.at(1).op_code, OpCode::INTERNALCALL);
EXPECT_EQ(instructions.at(1).operands.size(), 1);
EXPECT_EQ(instructions.at(1).operands.at(0), 4);

// INTERNALRETURN
EXPECT_EQ(instructions.at(5).op_code, OpCode::INTERNALRETURN);

auto trace = Execution::gen_trace(instructions, std::vector<FF>{});

// Expected sequence of PCs during execution
std::vector<FF> pc_sequence{ 0, 1, 4, 5, 2, 3 };

for (size_t i = 0; i < 6; i++) {
EXPECT_EQ(trace.at(i + 1).avmMini_pc, pc_sequence.at(i));
}

// Find the first row enabling the addition selector.
auto row = std::ranges::find_if(trace.begin(), trace.end(), [](Row r) { return r.avmMini_sel_op_add == 1; });
EXPECT_EQ(row->avmMini_ic, 345567789);

gen_proof_and_validate(bytecode, std::move(trace), std::vector<FF>{});
}

// Positive test with some nested internall calls
// We use the following functions (internal calls):
// F1: ADD(2,3,2) M[2] = M[2] + M[3]
// F2: MUL(2,3,2) M[2] = M[2] * M[3]
// G: F1 SET(17,3) F2 where SET(17,3) means M[3] = 17
// MAIN: SET(4,2) SET(7,3) G
// Whole execution should compute: (4 + 7) * 17 = 187
// Bytecode layout: SET(4,2) SET(7,3) INTERNAL_CALL_G RETURN BYTECODE(F2) BYTECODE(F1) BYTECODE(G)
// 0 1 2 3 4 6 8
// BYTECODE(F1): ADD(2,3,2) INTERNAL_RETURN
// BYTECODE(F2): MUL(2,3,2) INTERNAL_RETURN
// BYTECODE(G): INTERNAL_CALL(6) SET(17,3) INTERNAL_CALL(4) INTERNAL_RETURN
TEST_F(AvmMiniExecutionTests, nestedInternalCalls)
{
auto internalCallHex = [](std::string const& dst_offset) {
return "25"
"000000" +
dst_offset;
};

auto setHex = [](std::string const& val, std::string const& dst_offset) {
return "2701" // SET U8
+ val + "000000" + dst_offset;
};

const std::string tag_address_arguments = "01" // U8
"00000002" // addr a 2
"00000003" // addr b 3
"00000002"; // addr c 2

const std::string return_hex = "34" // RETURN
"00000000" // ret offset 0
"00000000"; // ret size 0

const std::string internal_ret_hex = "26";
const std::string add_hex = "00";
const std::string mul_hex = "02";

const std::string bytecode_f1 = add_hex + tag_address_arguments + internal_ret_hex;
const std::string bytecode_f2 = mul_hex + tag_address_arguments + internal_ret_hex;
const std::string bytecode_g =
internalCallHex("06") + setHex("11", "03") + internalCallHex("04") + internal_ret_hex;

std::string bytecode_hex = setHex("04", "02") + setHex("07", "03") + internalCallHex("08") + return_hex +
bytecode_f2 + bytecode_f1 + bytecode_g;

auto bytecode = hex_to_bytes(bytecode_hex);
auto instructions = Execution::parse(bytecode);

EXPECT_EQ(instructions.size(), 12);

// Expected sequence of opcodes
std::vector<OpCode> const opcode_sequence{ OpCode::SET, OpCode::SET,
OpCode::INTERNALCALL, OpCode::RETURN,
OpCode::MUL, OpCode::INTERNALRETURN,
OpCode::ADD, OpCode::INTERNALRETURN,
OpCode::INTERNALCALL, OpCode::SET,
OpCode::INTERNALCALL, OpCode::INTERNALRETURN };

for (size_t i = 0; i < 12; i++) {
EXPECT_EQ(instructions.at(i).op_code, opcode_sequence.at(i));
}

auto trace = Execution::gen_trace(instructions, std::vector<FF>{});

// Expected sequence of PCs during execution
std::vector<FF> pc_sequence{ 0, 1, 2, 8, 6, 7, 9, 10, 4, 5, 11, 3 };

for (size_t i = 0; i < 6; i++) {
EXPECT_EQ(trace.at(i + 1).avmMini_pc, pc_sequence.at(i));
}

// Find the first row enabling the multiplication selector.
auto row = std::ranges::find_if(trace.begin(), trace.end(), [](Row r) { return r.avmMini_sel_op_mul == 1; });
EXPECT_EQ(row->avmMini_ic, 187);
EXPECT_EQ(row->avmMini_pc, 4);

gen_proof_and_validate(bytecode, std::move(trace), std::vector<FF>{});
}

// Positive test with JUMP and CALLDATACOPY
// We test bytecode which first invoke CALLDATACOPY on a FF array of two values.
// Then, a JUMP call skips a SUB opcode to land to a DIV operation and RETURN.
// Calldata: [13, 156]
// Bytecode layout: CALLDATACOPY JUMP SUB DIV RETURN
// 0 1 2 3 4
TEST_F(AvmMiniExecutionTests, jumpAndCalldatacopy)
{
std::string bytecode_hex = "1F" // CALLDATACOPY 31 (no in_tag)
"00000000" // cd_offset
"00000002" // copy_size
"0000000A" // dst_offset // M[10] = 13, M[11] = 156
"23" // JUMP 35
"00000003" // jmp_dest (DIV located at 3)
"01" // SUB
"06" // FF
"0000000B" // addr 11
"0000000A" // addr 10
"00000001" // addr c 1 (If executed would be 156 - 13 = 143)
"03" // DIV
"06" // FF
"0000000B" // addr 11
"0000000A" // addr 10
"00000001" // addr c 1 (156 / 13 = 12)
"34" // RETURN
"00000000" // ret offset 0
"00000000" // ret size 0
;

auto bytecode = hex_to_bytes(bytecode_hex);
auto instructions = Execution::parse(bytecode);

EXPECT_EQ(instructions.size(), 5);

// We test parsing steps for CALLDATACOPY and JUMP.

// CALLDATACOPY
EXPECT_EQ(instructions.at(0).op_code, OpCode::CALLDATACOPY);
EXPECT_EQ(instructions.at(0).operands.size(), 3);
EXPECT_EQ(instructions.at(0).operands.at(0), 0);
EXPECT_EQ(instructions.at(0).operands.at(1), 2);
EXPECT_EQ(instructions.at(0).operands.at(2), 10);

// JUMP
EXPECT_EQ(instructions.at(1).op_code, OpCode::JUMP);
EXPECT_EQ(instructions.at(1).operands.size(), 1);
EXPECT_EQ(instructions.at(1).operands.at(0), 3);

auto trace = Execution::gen_trace(instructions, std::vector<FF>{ 13, 156 });

// Expected sequence of PCs during execution
std::vector<FF> pc_sequence{ 0, 1, 3, 4 };

for (size_t i = 0; i < 4; i++) {
EXPECT_EQ(trace.at(i + 1).avmMini_pc, pc_sequence.at(i));
}

// Find the first row enabling the division selector.
auto row = std::ranges::find_if(trace.begin(), trace.end(), [](Row r) { return r.avmMini_sel_op_div == 1; });
EXPECT_EQ(row->avmMini_ic, 12);

// Find the first row enabling the subtraction selector.
row = std::ranges::find_if(trace.begin(), trace.end(), [](Row r) { return r.avmMini_sel_op_sub == 1; });
// It must have failed as subtraction was "jumped over".
EXPECT_EQ(row, trace.end());

gen_proof_and_validate(bytecode, std::move(trace), std::vector<FF>{ 13, 156 });
}

// Negative test detecting an invalid opcode byte.
Expand Down

0 comments on commit fb1d742

Please sign in to comment.