Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
custom_bytecode.test.cpp
Go to the documentation of this file.
1#include <cstdint>
2#include <string>
3#include <utility>
4#include <vector>
5
6#include <gtest/gtest.h>
7
16
17namespace bb::avm2 {
18namespace {
19
20using simulation::Instruction;
21using testing::BytecodeBuilder;
22using testing::InstructionBuilder;
23using testing::PublicTxSimulationTester;
25using testing::TestEnqueuedCall;
26
27// A hand-built bytecode program together with its expected simulation outcome. Shared by the
28// simulation and proving phases below.
29struct CustomBytecodeCase {
30 std::string label;
31 std::vector<uint8_t> bytecode;
33};
34
35// The opcode byte value of the last valid wire opcode.
36constexpr uint8_t MAX_OPCODE_VALUE = static_cast<uint8_t>(WireOpCode::LAST_OPCODE_SENTINEL) - 1;
37// A tag byte value outside the valid range (valid tags are 0..MAX = U128 = 6).
38constexpr uint8_t INVALID_TAG = static_cast<uint8_t>(MemoryTag::MAX) + 1;
39
40Instruction set8(uint8_t dst, MemoryTag tag, uint8_t value)
41{
42 return InstructionBuilder(WireOpCode::SET_8).operand(dst).operand(tag).operand(value).build();
43}
44Instruction set32(uint16_t dst, MemoryTag tag, uint32_t value)
45{
46 return InstructionBuilder(WireOpCode::SET_32).operand(dst).operand(tag).operand(value).build();
47}
48Instruction set64(uint16_t dst, MemoryTag tag, uint64_t value)
49{
50 return InstructionBuilder(WireOpCode::SET_64).operand(dst).operand(tag).operand(value).build();
51}
52Instruction set128(uint16_t dst, MemoryTag tag, uint128_t value)
53{
54 return InstructionBuilder(WireOpCode::SET_128).operand(dst).operand(tag).operand(value).build();
55}
56Instruction setff(uint16_t dst, MemoryTag tag, const FF& value)
57{
58 return InstructionBuilder(WireOpCode::SET_FF).operand(dst).operand(tag).operand(value).build();
59}
60Instruction cast8(uint8_t src, uint8_t dst, MemoryTag tag)
61{
62 return InstructionBuilder(WireOpCode::CAST_8).operand(src).operand(dst).operand(tag).build();
63}
64Instruction ret(uint16_t copy_size_offset, uint16_t return_offset)
65{
66 return InstructionBuilder(WireOpCode::RETURN).operand(copy_size_offset).operand(return_offset).build();
67}
68Instruction jump(uint32_t loc)
69{
70 return InstructionBuilder(WireOpCode::JUMP_32).operand(loc).build();
71}
72
73CustomBytecodeCase avm_minimal()
74{
75 auto bytecode = BytecodeBuilder()
76 .add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/1))
77 .add(set8(/*dst=*/1, MemoryTag::U32, /*value=*/2))
78 .add(InstructionBuilder(WireOpCode::ADD_8)
79 .operand<uint8_t>(0)
80 .operand<uint8_t>(1)
81 .operand<uint8_t>(2)
82 .build())
83 .add(ret(/*copySizeOffset=*/0, /*returnOffset=*/2))
84 .build();
85 return { "AvmMinimal", std::move(bytecode), /*expect_revert=*/false };
86}
87
88// First instruction resolves a base address (offset 0) which is uninitialized (invalid tag).
89CustomBytecodeCase addressing_with_base_tag_issue(bool is_indirect)
90{
91 InstructionBuilder cdc(WireOpCode::CALLDATACOPY);
92 cdc.operand<uint16_t>(1).relative(); // copySize
93 if (is_indirect) {
94 cdc.indirect();
95 }
96 cdc.operand<uint16_t>(0); // cdOffset
97 cdc.operand<uint16_t>(0); // dstOffset
98 auto bytecode = BytecodeBuilder().add(cdc).add(ret(0, 0)).build();
99 return { is_indirect ? "AddressingWithBaseTagIssueIndirect" : "AddressingWithBaseTagIssueDirect",
101 /*expect_revert=*/true };
102}
103
104// A U64 value at offset 0 is used as an indirect address (invalid: must be U32).
105CustomBytecodeCase addressing_with_indirect_tag_issue()
106{
107 InstructionBuilder cdc(WireOpCode::CALLDATACOPY);
108 cdc.operand<uint16_t>(1).indirect(); // copySize: indirect
109 cdc.operand<uint16_t>(0); // cdOffset
110 cdc.operand<uint16_t>(1); // dstOffset
111 auto bytecode =
112 BytecodeBuilder().add(set64(/*dst=*/0, MemoryTag::U64, /*value=*/100)).add(cdc).add(ret(0, 0)).build();
113 return { "AddressingWithIndirectTagIssue", std::move(bytecode), /*expect_revert=*/true };
114}
115
116// Indirect addressing succeeds, then relative addressing fails due to a wrong base tag.
117CustomBytecodeCase addressing_with_indirect_then_relative_tag_issue()
118{
119 InstructionBuilder add(WireOpCode::ADD_16);
120 add.operand<uint16_t>(1).indirect(); // aOffset: indirect
121 add.operand<uint16_t>(2).relative(); // bOffset: relative
122 add.operand<uint16_t>(3); // dstOffset
123 auto bytecode =
124 BytecodeBuilder().add(set32(/*dst=*/1, MemoryTag::U32, /*value=*/10)).add(add).add(ret(0, 0)).build();
125 return { "AddressingWithIndirectThenRelativeTagIssue", std::move(bytecode), /*expect_revert=*/true };
126}
127
128// Relative addressing overflows (UINT32_MAX base) and indirect addressing also fails.
129CustomBytecodeCase addressing_with_relative_overflow_and_indirect_tag_issue()
130{
131 InstructionBuilder add(WireOpCode::ADD_8);
132 add.operand<uint8_t>(1).indirect().relative(); // aOffset: indirect-relative
133 add.operand<uint8_t>(2).indirect(); // bOffset: indirect
134 add.operand<uint8_t>(3); // dstOffset
135 auto bytecode =
136 BytecodeBuilder().add(set32(/*dst=*/0, MemoryTag::U32, /*value=*/0xffffffff)).add(add).add(ret(0, 0)).build();
137 return { "AddressingWithRelativeOverflowAndIndirectTagIssue", std::move(bytecode), /*expect_revert=*/true };
138}
139
140CustomBytecodeCase pc_out_of_range()
141{
142 auto bytecode = BytecodeBuilder().add(jump(/*loc=*/123)).add(ret(0, 0)).build();
143 return { "PcOutOfRange", std::move(bytecode), /*expect_revert=*/true };
144}
145
146CustomBytecodeCase invalid_opcode()
147{
148 const auto set_bytes = BytecodeBuilder().add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)).build();
149 const size_t return_opcode_offset = set_bytes.size();
150
151 auto bytecode = BytecodeBuilder().add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)).add(ret(0, 0)).build();
152 bytecode[return_opcode_offset] = MAX_OPCODE_VALUE + 1; // opcode out of range
153 return { "InvalidOpcode", std::move(bytecode), /*expect_revert=*/true };
154}
155
156CustomBytecodeCase invalid_byte()
157{
158 const uint8_t invalid_opcode = MAX_OPCODE_VALUE + 7;
159 return { "InvalidByte", { invalid_opcode }, /*expect_revert=*/true };
160}
161
162CustomBytecodeCase instruction_truncated()
163{
164 auto bytecode = BytecodeBuilder().add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)).build();
165 bytecode.pop_back(); // truncate last byte
166 return { "InstructionTruncated", std::move(bytecode), /*expect_revert=*/true };
167}
168
169CustomBytecodeCase invalid_tag_value()
170{
171 auto bytecode = BytecodeBuilder().add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)).add(ret(0, 0)).build();
172 const size_t tag_offset = tag_byte_offset(WireOpCode::SET_8);
173 bytecode[tag_offset] = INVALID_TAG;
174 return { "InvalidTagValue", std::move(bytecode), /*expect_revert=*/true };
175}
176
177CustomBytecodeCase invalid_tag_value_and_instruction_truncated()
178{
179 auto bytecode = BytecodeBuilder().add(set128(/*dst=*/0, MemoryTag::U128, /*value=*/0)).build();
180 bytecode.resize(bytecode.size() - 5); // truncate
181 const size_t tag_offset = tag_byte_offset(WireOpCode::SET_128);
182 bytecode[tag_offset] = 0x6f; // invalid tag value
183 return { "InvalidTagValueAndInstructionTruncated", std::move(bytecode), /*expect_revert=*/true };
184}
185
186CustomBytecodeCase set_truncation()
187{
188 // 200-bit value: forces truncation for every target tag up to U128.
189 const uint256_t low128 = (uint256_t(0x1234567890abcdefULL) << 64) | uint256_t(0x1234567890abcdefULL);
190 const FF large_field = FF((uint256_t(1) << 200) + low128);
191 // 40-bit value: forces truncation for target tags up to U32.
192 const uint64_t large_u64 = (uint64_t(1) << 40) + 0xdeadbeef;
193
194 auto bytecode = BytecodeBuilder()
195 .add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)) // Return copy-size slot.
196 .add(setff(/*dst=*/1, MemoryTag::U128, large_field))
197 .add(setff(/*dst=*/2, MemoryTag::U64, large_field))
198 .add(setff(/*dst=*/3, MemoryTag::U32, large_field))
199 .add(setff(/*dst=*/4, MemoryTag::U16, large_field))
200 .add(setff(/*dst=*/5, MemoryTag::U8, large_field))
201 .add(setff(/*dst=*/6, MemoryTag::U1, large_field))
202 .add(set64(/*dst=*/7, MemoryTag::U32, large_u64))
203 .add(set64(/*dst=*/8, MemoryTag::U16, large_u64))
204 .add(set64(/*dst=*/9, MemoryTag::U8, large_u64))
205 .add(set64(/*dst=*/10, MemoryTag::U1, large_u64))
206 .add(ret(/*copySizeOffset=*/0, /*returnOffset=*/0))
207 .build();
208 return { "SetTruncation", std::move(bytecode), /*expect_revert=*/false };
209}
210
211CustomBytecodeCase cast_truncation()
212{
213 const uint256_t low128 = (uint256_t(0x1234567890abcdefULL) << 64) | uint256_t(0x1234567890abcdefULL);
214 const FF large_field = FF((uint256_t(1) << 200) + low128);
215 const uint64_t large_u64 = (uint64_t(1) << 40) + 0xdeadbeef;
216
217 auto bytecode = BytecodeBuilder()
218 .add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)) // Return copy-size slot.
219 .add(setff(/*dst=*/10, MemoryTag::FF, large_field))
220 .add(cast8(/*src=*/10, /*dst=*/11, MemoryTag::U128))
221 .add(cast8(/*src=*/10, /*dst=*/12, MemoryTag::U64))
222 .add(cast8(/*src=*/10, /*dst=*/13, MemoryTag::U32))
223 .add(cast8(/*src=*/10, /*dst=*/14, MemoryTag::U16))
224 .add(cast8(/*src=*/10, /*dst=*/15, MemoryTag::U8))
225 .add(cast8(/*src=*/10, /*dst=*/16, MemoryTag::U1))
226 .add(set64(/*dst=*/20, MemoryTag::U64, large_u64))
227 .add(cast8(/*src=*/20, /*dst=*/21, MemoryTag::U32))
228 .add(cast8(/*src=*/20, /*dst=*/22, MemoryTag::U16))
229 .add(cast8(/*src=*/20, /*dst=*/23, MemoryTag::U8))
230 .add(cast8(/*src=*/20, /*dst=*/24, MemoryTag::U1))
231 .add(ret(/*copySizeOffset=*/0, /*returnOffset=*/0))
232 .build();
233 return { "CastTruncation", std::move(bytecode), /*expect_revert=*/false };
234}
235
236// A SET_FF instruction whose 32-byte FF immediate encodes a value larger than the field modulus
237// (FF::modulus + 25). The deserializer reads the immediate into a field element, which reduces it
238// modulo the field modulus, so the resolved value is 25, the instruction is accepted, and execution
239// succeeds. The FF type cannot represent a value >= the modulus (it reduces on construction), so we
240// build a valid SET_FF first and then overwrite the immediate bytes directly with the overflowing
241// value.
242CustomBytecodeCase set_field_overflow()
243{
244 auto bytecode = BytecodeBuilder()
245 .add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)) // Return copy-size slot.
246 .add(setff(/*dst=*/1, MemoryTag::FF, FF(25)))
247 .add(ret(/*copySizeOffset=*/0, /*returnOffset=*/0))
248 .build();
249
250 // The FF immediate is the trailing operand of the SET_FF instruction, which itself follows the
251 // leading SET_8. Locate it from the serialized instruction sizes rather than hard-coding offsets.
253 const size_t set8_size = BytecodeBuilder().add(set8(/*dst=*/0, MemoryTag::U32, /*value=*/0)).size();
254 const size_t setff_size = BytecodeBuilder().add(setff(/*dst=*/1, MemoryTag::FF, FF(25))).size();
255 const size_t ff_offset = set8_size + (setff_size - ff_width);
256
257 // Overwrite the big-endian FF immediate with a value that overflows the field modulus.
259 for (size_t i = 0; i < ff_width; ++i) {
260 bytecode[ff_offset + (ff_width - 1 - i)] = static_cast<uint8_t>(value);
261 value >>= 8;
262 }
263
264 return { "SetFieldOverflow", std::move(bytecode), /*expect_revert=*/false };
265}
266
267// All custom-bytecode cases: a minimal happy path, the addressing/bytecode-flow unhappy paths
268// (which must exceptionally halt), and the SET/CAST truncation happy paths.
269std::vector<CustomBytecodeCase> get_custom_bytecode_cases()
270{
272 cases.push_back(avm_minimal());
273 cases.push_back(addressing_with_base_tag_issue(/*is_indirect=*/true));
274 cases.push_back(addressing_with_base_tag_issue(/*is_indirect=*/false));
275 cases.push_back(addressing_with_indirect_tag_issue());
276 cases.push_back(addressing_with_indirect_then_relative_tag_issue());
277 cases.push_back(addressing_with_relative_overflow_and_indirect_tag_issue());
278 cases.push_back(pc_out_of_range());
279 cases.push_back(invalid_opcode());
280 cases.push_back(invalid_byte());
281 cases.push_back(instruction_truncated());
282 cases.push_back(invalid_tag_value());
283 cases.push_back(invalid_tag_value_and_instruction_truncated());
284 cases.push_back(set_truncation());
285 cases.push_back(cast_truncation());
286 cases.push_back(set_field_overflow());
287 return cases;
288}
289
290PublicSimulatorConfig proving_config()
291{
292 PublicSimulatorConfig config = PublicTxSimulationTester::default_config();
293 config.collect_hints = true;
294 config.collect_public_inputs = true;
295 return config;
296}
297
298class CustomBytecodeSimulation : public ::testing::TestWithParam<CustomBytecodeCase> {};
299
300// Each custom-bytecode program is run through the full pipeline: fast simulation (checking the
301// expected revert behavior), simulation with hint generation, and proving (check circuit).
302TEST_P(CustomBytecodeSimulation, SimulateAndProve)
303{
304 const CustomBytecodeCase& test_case = GetParam();
305 PublicTxSimulationTester tester;
306 const auto deployed = tester.deploy_contract(test_case.bytecode);
307
308 // 1. Fast simulation.
309 const TxSimulationResult fast_result =
310 tester.simulate_tx({ TestEnqueuedCall{ .contract_address = deployed.address } });
311 EXPECT_EQ(fast_result.revert_code != RevertCode::OK, test_case.expect_revert);
312
313 // 2. Simulation for hint generation.
314 const TxSimulationResult hint_result =
315 tester.simulate_tx({ TestEnqueuedCall{ .contract_address = deployed.address } }, proving_config());
316 ASSERT_TRUE(hint_result.public_inputs.has_value());
317 ASSERT_TRUE(hint_result.hints.has_value());
318
319 // 3. Proving (check circuit).
320 const AvmProvingInputs proving_inputs{ .public_inputs = *hint_result.public_inputs, .hints = *hint_result.hints };
321 AvmAPI api;
322 EXPECT_TRUE(api.check_circuit(proving_inputs));
323}
324
325INSTANTIATE_TEST_SUITE_P(CustomBytecode,
326 CustomBytecodeSimulation,
327 ::testing::ValuesIn(get_custom_bytecode_cases()),
328 [](const ::testing::TestParamInfo<CustomBytecodeCase>& info) { return info.param.label; });
329
330} // namespace
331} // namespace bb::avm2
INSTANTIATE_TEST_SUITE_P(All, KernelIOTamperingTests, testing::Values(KernelIOField::PAIRING_INPUTS, KernelIOField::ACCUMULATOR_HASH, KernelIOField::KERNEL_RETURN_DATA, KernelIOField::APP_RETURN_DATA, KernelIOField::ECC_OP_HASH), [](const testing::TestParamInfo< KernelIOField > &info) { switch(info.param) { case KernelIOField::PAIRING_INPUTS:return "PairingInputs";case KernelIOField::ACCUMULATOR_HASH:return "AccumulatorHash";case KernelIOField::KERNEL_RETURN_DATA:return "KernelReturnData";case KernelIOField::APP_RETURN_DATA:return "AppReturnData";case KernelIOField::ECC_OP_HASH:return "EccOpHash";} return "Unknown";})
TEST_P(KernelIOTamperingTests, CausesVerificationFailure)
#define info(...)
Definition log.hpp:93
std::string label
std::vector< uint8_t > bytecode
bool expect_revert
const std::unordered_map< OperandType, uint32_t > & get_operand_type_sizes()
size_t tag_byte_offset(WireOpCode opcode)
AvmFlavorSettings::FF FF
Definition field.hpp:10
Instruction
Enumeration of VM instructions that can be executed.
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
unsigned __int128 uint128_t
Definition serialize.hpp:45
static constexpr uint256_t modulus