Skip to content

Commit 1d981c8

Browse files
authored
feat: merge-train/avm (#19248)
BEGIN_COMMIT_OVERRIDE feat(avm): tx fuzzer docker container (#19133) fix(avm): sha256 tracegen batched tag checks (#19244) fix: claude-generated pre-audit of avm contract instance manager - completeness bug (#19254) END_COMMIT_OVERRIDE
2 parents be5f6ab + cc801d3 commit 1d981c8

File tree

8 files changed

+661
-14
lines changed

8 files changed

+661
-14
lines changed

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/contract_instance_manager.cpp

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,30 @@ std::optional<ContractInstance> ContractInstanceManager::get_contract_instance(c
3838
std::optional<ContractInstance> maybe_instance = contract_db.get_contract_instance(contract_address);
3939

4040
const auto& tree_state = merkle_db.get_tree_state();
41-
// If this is a canonical address
41+
42+
// Check if this is a protocol contract address (addresses 1 to MAX_PROTOCOL_CONTRACTS).
43+
// Protocol contracts are special reserved addresses that don't require nullifier checks.
4244
if (ff_gt.ff_gt(MAX_PROTOCOL_CONTRACTS, contract_address - 1)) {
43-
// Handle protocol contract addresses
45+
// Handle protocol contract addresses.
46+
// The derived_address lookup returns nullopt if this protocol contract slot is empty.
47+
// NOTE: MAX_PROTOCOL_CONTRACTS (currently 11) is the reserved capacity, but not all
48+
// slots may be filled. For example, addresses 1-6 are currently used while 7-11 are
49+
// empty (reserved for future protocol contracts).
4450
std::optional<AztecAddress> derived_address = get_derived_address(protocol_contracts, contract_address);
51+
52+
// Sanity check: if we found a derived address, we should also have the instance, and vice versa.
4553
assert(derived_address.has_value() == maybe_instance.has_value() &&
4654
"Derived address should be found if the instance was retrieved and vice versa");
47-
const ContractInstance& instance = maybe_instance.value();
55+
4856
event_emitter.emit({
4957
.address = contract_address,
50-
.contract_instance = maybe_instance.value_or<ContractInstance>({}),
51-
// Tree context
58+
.contract_instance = maybe_instance.value_or(ContractInstance{}),
5259
.nullifier_tree_root = tree_state.nullifier_tree.tree.root,
5360
.public_data_tree_root = tree_state.public_data_tree.tree.root,
5461
.exists = derived_address.has_value(),
5562
.is_protocol_contract = true,
5663
});
57-
58-
return instance;
64+
return maybe_instance;
5965
}
6066

6167
if (!merkle_db.nullifier_exists(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, contract_address)) {
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#include "barretenberg/vm2/simulation/gadgets/contract_instance_manager.hpp"
2+
3+
#include <gmock/gmock.h>
4+
#include <gtest/gtest.h>
5+
#include <optional>
6+
7+
#include "barretenberg/vm2/common/aztec_constants.hpp"
8+
#include "barretenberg/vm2/common/aztec_types.hpp"
9+
#include "barretenberg/vm2/simulation/events/contract_instance_retrieval_event.hpp"
10+
#include "barretenberg/vm2/simulation/events/event_emitter.hpp"
11+
#include "barretenberg/vm2/simulation/testing/mock_dbs.hpp"
12+
#include "barretenberg/vm2/simulation/testing/mock_field_gt.hpp"
13+
#include "barretenberg/vm2/simulation/testing/mock_update_check.hpp"
14+
#include "barretenberg/vm2/testing/fixtures.hpp"
15+
16+
using ::testing::_;
17+
using ::testing::Return;
18+
using ::testing::SizeIs;
19+
using ::testing::StrictMock;
20+
21+
namespace bb::avm2::simulation {
22+
namespace {
23+
24+
class ContractInstanceManagerTest : public ::testing::Test {
25+
protected:
26+
StrictMock<MockContractDB> contract_db;
27+
StrictMock<MockHighLevelMerkleDB> merkle_db;
28+
StrictMock<MockUpdateCheck> update_check;
29+
StrictMock<MockFieldGreaterThan> field_gt;
30+
EventEmitter<ContractInstanceRetrievalEvent> event_emitter;
31+
};
32+
33+
// Test that querying an empty protocol contract slot (addresses 7-11 are currently unused)
34+
// correctly returns nullopt with exists=false, rather than crashing.
35+
TEST_F(ContractInstanceManagerTest, EmptyProtocolContractSlotReturnsNullopt)
36+
{
37+
// Create protocol contracts with only first 6 slots filled (matching real configuration)
38+
// Slots 7-11 are zero (empty).
39+
ProtocolContracts protocol_contracts;
40+
for (uint32_t i = 0; i < 6; i++) {
41+
// Fill slots 0-5 (addresses 1-6) with non-zero derived addresses
42+
protocol_contracts.derived_addresses[i] = AztecAddress(FF(0x1000 + i));
43+
}
44+
// Slots 6-10 (addresses 7-11) are left as zero (default)
45+
46+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
47+
48+
// Query address 7 - an empty protocol contract slot
49+
AztecAddress empty_slot_address = AztecAddress(7);
50+
51+
// Setup mocks
52+
TreeStates tree_states = {};
53+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
54+
55+
// The contract DB should return nullopt since the instance doesn't exist
56+
EXPECT_CALL(contract_db, get_contract_instance(empty_slot_address)).WillOnce(Return(std::nullopt));
57+
58+
// ff_gt(MAX_PROTOCOL_CONTRACTS=11, 7-1=6) returns true because 11 > 6
59+
// This means address 7 IS in the protocol contract range
60+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), FF(6))).WillOnce(Return(true));
61+
62+
// The call should NOT crash and should return nullopt
63+
auto result = manager.get_contract_instance(empty_slot_address);
64+
65+
// Verify the result
66+
EXPECT_FALSE(result.has_value());
67+
68+
// Verify the event was emitted correctly
69+
auto events = event_emitter.dump_events();
70+
ASSERT_THAT(events, SizeIs(1));
71+
EXPECT_EQ(events[0].address, empty_slot_address);
72+
EXPECT_FALSE(events[0].exists);
73+
EXPECT_TRUE(events[0].is_protocol_contract);
74+
EXPECT_EQ(events[0].contract_instance, ContractInstance{});
75+
}
76+
77+
// Test that a valid protocol contract (e.g., address 1) works correctly
78+
TEST_F(ContractInstanceManagerTest, ValidProtocolContractReturnsInstance)
79+
{
80+
// Create protocol contracts with first slot filled
81+
ProtocolContracts protocol_contracts;
82+
AztecAddress derived_addr = AztecAddress(FF(0x12345));
83+
protocol_contracts.derived_addresses[0] = derived_addr; // Address 1 -> index 0
84+
85+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
86+
87+
// Query address 1 - a valid protocol contract
88+
AztecAddress protocol_address = AztecAddress(1);
89+
90+
// Create a contract instance
91+
ContractInstance instance = testing::random_contract_instance();
92+
93+
// Setup mocks
94+
TreeStates tree_states = {};
95+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
96+
EXPECT_CALL(contract_db, get_contract_instance(protocol_address)).WillOnce(Return(instance));
97+
98+
// ff_gt(MAX_PROTOCOL_CONTRACTS=11, 1-1=0) returns true because 11 > 0
99+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), FF(0))).WillOnce(Return(true));
100+
101+
// The call should succeed and return the instance
102+
auto result = manager.get_contract_instance(protocol_address);
103+
104+
// Verify the result
105+
ASSERT_TRUE(result.has_value());
106+
EXPECT_EQ(result.value(), instance);
107+
108+
// Verify the event was emitted correctly
109+
auto events = event_emitter.dump_events();
110+
ASSERT_THAT(events, SizeIs(1));
111+
EXPECT_EQ(events[0].address, protocol_address);
112+
EXPECT_TRUE(events[0].exists);
113+
EXPECT_TRUE(events[0].is_protocol_contract);
114+
EXPECT_EQ(events[0].contract_instance, instance);
115+
}
116+
117+
// Test that a regular (non-protocol) contract that exists works correctly
118+
TEST_F(ContractInstanceManagerTest, RegularContractExistsReturnsInstance)
119+
{
120+
ProtocolContracts protocol_contracts; // Empty - no protocol contracts
121+
122+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
123+
124+
// Query a regular address (outside protocol contract range)
125+
AztecAddress regular_address = AztecAddress(FF(0x1234567890ULL));
126+
127+
// Create a contract instance
128+
ContractInstance instance = testing::random_contract_instance();
129+
130+
// Setup mocks
131+
TreeStates tree_states = {};
132+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
133+
EXPECT_CALL(contract_db, get_contract_instance(regular_address)).WillOnce(Return(instance));
134+
135+
// ff_gt(11, large_address - 1) returns false - not in protocol range
136+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), regular_address - 1)).WillOnce(Return(false));
137+
138+
// Nullifier exists - contract is deployed
139+
EXPECT_CALL(merkle_db, nullifier_exists(FF(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS), regular_address))
140+
.WillOnce(Return(true));
141+
142+
// Update check is performed for regular contracts
143+
EXPECT_CALL(update_check, check_current_class_id(regular_address, instance));
144+
145+
// The call should succeed
146+
auto result = manager.get_contract_instance(regular_address);
147+
148+
// Verify the result
149+
ASSERT_TRUE(result.has_value());
150+
EXPECT_EQ(result.value(), instance);
151+
152+
// Verify the event
153+
auto events = event_emitter.dump_events();
154+
ASSERT_THAT(events, SizeIs(1));
155+
EXPECT_EQ(events[0].address, regular_address);
156+
EXPECT_TRUE(events[0].exists);
157+
EXPECT_FALSE(events[0].is_protocol_contract);
158+
}
159+
160+
// Test that a regular contract that doesn't exist returns nullopt
161+
TEST_F(ContractInstanceManagerTest, RegularContractNotExistsReturnsNullopt)
162+
{
163+
ProtocolContracts protocol_contracts; // Empty
164+
165+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
166+
167+
AztecAddress non_existent_address = AztecAddress(FF(0xDEADBEEFULL));
168+
169+
// Setup mocks
170+
TreeStates tree_states = {};
171+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
172+
EXPECT_CALL(contract_db, get_contract_instance(non_existent_address)).WillOnce(Return(std::nullopt));
173+
174+
// Not in protocol range
175+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), non_existent_address - 1)).WillOnce(Return(false));
176+
177+
// Nullifier doesn't exist - contract not deployed
178+
EXPECT_CALL(merkle_db, nullifier_exists(FF(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS), non_existent_address))
179+
.WillOnce(Return(false));
180+
181+
// The call should return nullopt
182+
auto result = manager.get_contract_instance(non_existent_address);
183+
184+
EXPECT_FALSE(result.has_value());
185+
186+
// Verify the event
187+
auto events = event_emitter.dump_events();
188+
ASSERT_THAT(events, SizeIs(1));
189+
EXPECT_EQ(events[0].address, non_existent_address);
190+
EXPECT_FALSE(events[0].exists);
191+
EXPECT_FALSE(events[0].is_protocol_contract);
192+
}
193+
194+
} // namespace
195+
} // namespace bb::avm2::simulation

barretenberg/cpp/src/barretenberg/vm2/tracegen/sha256_trace.cpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,17 +404,21 @@ void Sha256TraceBuilder::process(
404404

405405
if (invalid_state_tag_err) {
406406
// This is the more efficient batched tag check we perform in the circuit
407-
uint64_t batched_check = 0;
407+
FF batched_tag_check = 0;
408408
// Batch the state tag checks
409+
FF target_tag = FF(static_cast<uint8_t>(MemoryTag::U32));
409410
for (uint32_t i = 0; i < event.state.size(); i++) {
410-
batched_check |=
411-
(static_cast<uint64_t>(event.state[i].get_tag()) - static_cast<uint64_t>(MemoryTag::U32))
412-
<< (i * 3);
411+
// Compute the batched tag check step by step to match the circuit implementation
412+
FF mem_tag = FF(static_cast<uint8_t>(event.state[i].get_tag()));
413+
FF state_tag_diff = mem_tag - target_tag;
414+
FF exponent = FF(1 << (i * 3)); // exponent is 1, 8, 64, 512, ...
415+
batched_tag_check += state_tag_diff * exponent;
413416
}
414417
trace.set(row,
415418
{ {
416419
{ C::sha256_sel_invalid_state_tag_err, 1 },
417-
{ C::sha256_batch_tag_inv, FF(batched_check).invert() },
420+
// Guaranteed non-zero (so inversion is safe) since we have an invalid tag
421+
{ C::sha256_batch_tag_inv, FF(batched_tag_check).invert() },
418422
{ C::sha256_latch, 1 },
419423
{ C::sha256_err, 1 }, // Set the error flag
420424
} });
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
crash-reports/
2+
output/
3+
corpus/
4+
artifacts/
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
IFS=$'\n\t'
5+
6+
timeout='2592000' # 1 month
7+
cpus='4'
8+
mem="8G"
9+
jobs_="$cpus"
10+
workers='1'
11+
max_len='8192'
12+
13+
show_help() {
14+
echo "Usage: $0 [options]"
15+
echo ""
16+
echo "Options:"
17+
echo " -t, --timeout <timeout> Set the maximum total time for fuzzing in seconds (default: $timeout - 1 month)"
18+
echo " -c, --cpus <cpus> Set the amount of CPUs for container to use (default: $cpus)"
19+
echo " --mem <memory> Set the amount of memory for container to use (default: $mem)"
20+
echo " -j, --jobs <N> Set the amount of processes to run (default: $jobs_)"
21+
echo " -w, --workers <N> Set the amount of subprocesses per job (default: $workers)"
22+
echo " -m, --max-len <N> Set the maximum input length in bytes (default: $max_len)"
23+
echo " -h, --help Display this help and exit"
24+
echo " -- Pass additional arguments to the fuzzer"
25+
echo ""
26+
echo "This script builds and runs the AVM TX fuzzer container."
27+
}
28+
29+
EXTRA_ARGS=()
30+
31+
while [[ $# -gt 0 ]]; do
32+
case "$1" in
33+
-t | --timeout)
34+
timeout="$2"
35+
shift 2
36+
;;
37+
-w | --workers)
38+
workers="$2"
39+
shift 2
40+
;;
41+
-j | --jobs)
42+
jobs_="$2"
43+
shift 2
44+
;;
45+
-c | --cpus)
46+
cpus="$2"
47+
shift 2
48+
;;
49+
--mem)
50+
mem="$2"
51+
shift 2
52+
;;
53+
-m | --max-len)
54+
max_len="$2"
55+
shift 2
56+
;;
57+
-h | --help)
58+
show_help
59+
exit 0
60+
;;
61+
--)
62+
shift
63+
EXTRA_ARGS=("$@")
64+
break
65+
;;
66+
-*)
67+
echo "Error: Unsupported flag $1" >&2
68+
exit 1
69+
;;
70+
*)
71+
break
72+
;;
73+
esac
74+
done
75+
76+
image_name=avm-tx-fuzzer
77+
78+
# Get the directory where this script is located
79+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
80+
# Get aztec-packages root (two levels up from container-builds/avm-fuzzing-container)
81+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
82+
83+
echo "Building container image: $image_name"
84+
echo "Build context: $REPO_ROOT"
85+
docker build -t "$image_name":latest -f "$SCRIPT_DIR/src/Dockerfile" "$REPO_ROOT"
86+
87+
mkdir -p crash-reports output corpus artifacts
88+
89+
docker_args=(
90+
--rm
91+
--user root
92+
-v "$(pwd)/crash-reports:/home/fuzzer/crash-reports:rw"
93+
-v "$(pwd)/output:/home/fuzzer/output:rw"
94+
-v "$(pwd)/corpus:/home/fuzzer/corpus:rw"
95+
-v "$(pwd)/artifacts:/home/fuzzer/artifacts:rw"
96+
--cpus="$cpus"
97+
-m "$mem"
98+
)
99+
100+
# Add -it only if we're in an interactive terminal
101+
if [ -t 0 ] && [ -t 1 ]; then
102+
docker_args=(-it "${docker_args[@]}")
103+
fi
104+
105+
docker_args+=("$image_name")
106+
107+
entrypoint_args=(
108+
--timeout "$timeout"
109+
--workers "$workers"
110+
--jobs "$jobs_"
111+
--max-len "$max_len"
112+
)
113+
114+
# Add extra arguments after --
115+
if [ ${#EXTRA_ARGS[@]} -gt 0 ]; then
116+
entrypoint_args+=(-- "${EXTRA_ARGS[@]}")
117+
fi
118+
119+
echo "Starting AVM TX fuzzer container..."
120+
docker run "${docker_args[@]}" "${entrypoint_args[@]}"

0 commit comments

Comments
 (0)