diff --git a/barretenberg/cpp/src/barretenberg/api/aztec_process.cpp b/barretenberg/cpp/src/barretenberg/api/aztec_process.cpp index 6663060f993c..671f72dfd9a7 100644 --- a/barretenberg/cpp/src/barretenberg/api/aztec_process.cpp +++ b/barretenberg/cpp/src/barretenberg/api/aztec_process.cpp @@ -258,6 +258,21 @@ bool process_aztec_artifact(const std::string& input_path, const std::string& ou return true; } + // Strip __aztec_nr_internals__ prefix from function names. + // The #[aztec] macro generates wrapper functions with this prefix; we strip it so + // the exported ABI exposes the original developer-written names. + const std::string internal_prefix = "__aztec_nr_internals__"; + for (auto& function : artifact_json["functions"]) { + auto& name = function["name"]; + if (name.is_string()) { + std::string fn_name = name.get(); + if (fn_name.size() >= internal_prefix.size() && + fn_name.compare(0, internal_prefix.size(), internal_prefix) == 0) { + name = fn_name.substr(internal_prefix.size()); + } + } + } + // Filter to private constrained functions std::vector private_functions; for (auto& function : artifact_json["functions"]) { @@ -266,14 +281,13 @@ bool process_aztec_artifact(const std::string& input_path, const std::string& ou } } - if (private_functions.empty()) { + if (!private_functions.empty()) { + // Generate VKs + generate_vks_for_functions(cache_dir, private_functions, force); + } else { info("No private constrained functions found"); - return true; } - // Generate VKs - generate_vks_for_functions(cache_dir, private_functions, force); - // Write updated JSON back to file std::ofstream out_file(output_path); out_file << artifact_json.dump(2) << std::endl; diff --git a/docs/bootstrap.sh b/docs/bootstrap.sh index 3400d0ce9ef8..38aa836d5973 100755 --- a/docs/bootstrap.sh +++ b/docs/bootstrap.sh @@ -4,7 +4,6 @@ source $(git rev-parse --show-toplevel)/ci3/source_bootstrap repo_root=$(git rev-parse --show-toplevel) export BB=${BB:-$repo_root/barretenberg/cpp/build/bin/bb} export NARGO=${NARGO:-$repo_root/noir/noir-repo/target/release/nargo} -export TRANSPILER=${TRANSPILER:-$repo_root/avm-transpiler/target/release/avm-transpiler} export BB_HASH=${BB_HASH:-$($repo_root/barretenberg/cpp/bootstrap.sh hash)} # We search the docs/*.md files to find included code, and use those as our rebuild dependencies. diff --git a/docs/examples/bootstrap.sh b/docs/examples/bootstrap.sh index b112eceec7eb..4ff015947d76 100755 --- a/docs/examples/bootstrap.sh +++ b/docs/examples/bootstrap.sh @@ -6,8 +6,6 @@ REPO_ROOT=$(git rev-parse --show-toplevel) export BB=${BB:-"$REPO_ROOT/barretenberg/cpp/build/bin/bb"} export NARGO=${NARGO:-"$REPO_ROOT/noir/noir-repo/target/release/nargo"} -export TRANSPILER=${TRANSPILER:-"$REPO_ROOT/avm-transpiler/target/release/avm-transpiler"} -export STRIP_AZTEC_NR_PREFIX=${STRIP_AZTEC_NR_PREFIX:-"$REPO_ROOT/noir-projects/noir-contracts/scripts/strip_aztec_nr_prefix.sh"} export BB_HASH=${BB_HASH:-$("$REPO_ROOT/barretenberg/cpp/bootstrap.sh" hash)} export NOIR_HASH=${NOIR_HASH:-$("$REPO_ROOT/noir/bootstrap.sh" hash)} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/auth_witness.nr b/noir-projects/aztec-nr/aztec/src/oracle/auth_witness.nr index 57a54b5f7f35..b850be1dd3bd 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/auth_witness.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/auth_witness.nr @@ -5,3 +5,37 @@ unconstrained fn get_auth_witness_oracle(_message_hash: Field) -> [F pub unconstrained fn get_auth_witness(message_hash: Field) -> [Field; N] { get_auth_witness_oracle(message_hash) } + +/// Fetches an auth witness and casts each field to a byte. +/// +/// Each field is range-checked to `[0, 256)` before casting to prevent silent truncation (e.g. a field value of +/// `b + 256` would truncate to the same byte as `b`). +pub unconstrained fn get_auth_witness_as_bytes(message_hash: Field) -> [u8; N] { + let witness = get_auth_witness::(message_hash); + let mut result: [u8; N] = [0; N]; + for i in 0..N { + assert(witness[i].lt(256), "auth witness field is not a single byte"); + result[i] = witness[i] as u8; + } + result +} + +mod test { + use super::get_auth_witness_as_bytes; + use std::test::OracleMock; + + #[test] + unconstrained fn get_auth_witness_as_bytes_casts_valid_witness() { + let witness: [Field; 3] = [0, 127, 255]; + let _ = OracleMock::mock("aztec_utl_getAuthWitness").returns(witness); + let bytes: [u8; 3] = get_auth_witness_as_bytes(0); + assert_eq(bytes, [0, 127, 255]); + } + + #[test(should_fail_with = "auth witness field is not a single byte")] + unconstrained fn get_auth_witness_as_bytes_rejects_field_above_byte_range() { + let witness: [Field; 1] = [256]; + let _ = OracleMock::mock("aztec_utl_getAuthWitness").returns(witness); + let _: [u8; 1] = get_auth_witness_as_bytes(0); + } +} diff --git a/noir-projects/noir-contracts/bootstrap.sh b/noir-projects/noir-contracts/bootstrap.sh index 64252a9d7088..b88ea473f464 100755 --- a/noir-projects/noir-contracts/bootstrap.sh +++ b/noir-projects/noir-contracts/bootstrap.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# TODO: THIS SCRIPT SHOULD NOW BE ABLE TO REPLACE TRANSPILATION AND VK GENERATION WITH 'bb aztec_process'. # # Some notes if you have to work on this script. # - First of all, I'm sorry (edit: not sorry). It's a beautiful script but it's no fun to debug. I got carried away. @@ -30,77 +29,12 @@ export PLATFORM_TAG=any export BB=${BB:-../../barretenberg/cpp/build/bin/bb} export NARGO=${NARGO:-../../noir/noir-repo/target/release/nargo} -export TRANSPILER=${TRANSPILER:-../../avm-transpiler/target/release/avm-transpiler} -export STRIP_AZTEC_NR_PREFIX=${STRIP_AZTEC_NR_PREFIX:-./scripts/strip_aztec_nr_prefix.sh} export BB_HASH=${BB_HASH:-$(../../barretenberg/cpp/bootstrap.sh hash)} export NOIR_HASH=${NOIR_HASH:-$(../../noir/bootstrap.sh hash)} -export tmp_dir=./target/tmp - -# Remove our tmp dir from last run. -# Note: This can use BASH 'trap' for better cleanliness, but the script has been hitting edge-cases so is (temporarily?) simplified. -rm -rf $tmp_dir -mkdir -p $tmp_dir - # Set common flags for parallel. export PARALLEL_FLAGS="-j${PARALLELISM:-16} --halt now,fail=1 --memsuspend $(memsuspend_limit)" -# This computes a vk and adds it to the input function json if it's private, else returns same input. -# stdin has the function json. -# stdout receives the function json with the vk added (if private). -# The function is exported and called by a sub-shell in parallel, so we must "set -eu" etc.. -# If debugging, a set -x at the start can help. -function process_function { - set -euo pipefail - local func name bytecode_b64 hash vk - - contract_hash=$1 - # Read the function json. - func="$(cat)" - name=$(echo "$func" | jq -r '.name') - echo_stderr "Processing function: $name..." - - # Check if the function is neither public nor unconstrained. - # TODO: Why do we need to gen keys for functions that are not marked private? - # We allow the jq call to error (set +e) because it returns an error code if the result is false. - # We then differentiate between a real error, and the result being false. - set +e - make_vk=$(echo "$func" | jq -e '(.custom_attributes | index("public") == null) and (.is_unconstrained == false)') - if [ $? -ne 0 ] && [ "$make_vk" != "false" ]; then - echo_stderr "Failed to check function $name is neither public nor unconstrained." - exit 1 - fi - set -e - - if [ "$make_vk" == "true" ]; then - # It's a private function. - # Build hash, check if in cache. - # If it's in the cache it's extracted to $tmp_dir/$hash - bytecode_b64=$(echo "$func" | jq -r '.bytecode') - hash=$((echo "$BB_HASH"; echo "$bytecode_b64") | sha256sum | tr -d ' -') - - if ! cache_download vk-$contract_hash-$hash.tar.gz >&2; then - # It's not in the cache. Generate the vk file and upload it to the cache. - echo_stderr "Generating vk for function: $name..." - - local outdir=$(mktemp -d -p $tmp_dir) - echo "$bytecode_b64" | base64 -d | gunzip | $BB write_vk --scheme chonk -b - -o $outdir -v - mv $outdir/vk $tmp_dir/$contract_hash/$hash - - cache_upload vk-$contract_hash-$hash.tar.gz $tmp_dir/$contract_hash/$hash - fi - - # Return (echo) json containing the base64 encoded verification key. - vk=$(cat $tmp_dir/$contract_hash/$hash | base64 -w 0) - echo "$func" | jq -c --arg vk "$vk" '. + {verification_key: $vk}' - else - echo_stderr "Function $name is neither public nor unconstrained, skipping." - # Not a private function. Return the original function json. - echo "$func" - fi -} -export -f process_function - # Compute hash for a given contract. # $1 is the contract name, $2 is the folder name (e.g. "contracts" or "examples") function get_contract_hash { @@ -159,51 +93,26 @@ function get_contract_path { } export -f get_contract_path -# This compiles a noir contract, transpile's public functions, and generates vk's for private functions. +# This compiles a noir contract, transpiles public functions, strips internal prefixes, +# and generates verification keys for private functions via 'bb aztec_process'. # $1 is the input package name, $2 is the folder name (e.g. "contracts" or "examples") -# On exit it's fully processed json artifact is in the target dir. +# On exit its fully processed json artifact is in the target dir. # The function is exported and called by a sub-shell in parallel, so we must "set -eu" etc.. function compile { set -euo pipefail - local contract_name contract_hash local contract_path=$(get_contract_path "$1" "$2") local contract=$(grep -oP '(?<=^name = ")[^"]+' "$2/$contract_path/Nargo.toml") # Calculate filename because nargo... - contract_name=$(cat $2/$contract_path/src/main.nr | awk '/^contract / { print $2 } /^pub contract / { print $3 }') + local contract_name=$(cat $2/$contract_path/src/main.nr | awk '/^contract / { print $2 } /^pub contract / { print $3 }') local filename="$contract-$contract_name.json" local json_path="./target/$filename" - contract_hash=$(get_contract_hash $1 $2) + local contract_hash=$(get_contract_hash $1 $2) if ! cache_download contract-$contract_hash.tar.gz; then - $NARGO compile --package $contract --inliner-aggressiveness 0 --deny-warnings - $TRANSPILER $json_path $json_path - $STRIP_AZTEC_NR_PREFIX $json_path + $NARGO compile --package $contract --inliner-aggressiveness 0 --deny-warnings + $BB aztec_process -i $json_path cache_upload contract-$contract_hash.tar.gz $json_path fi - - # We segregate equivalent vk's created by process_function. This was done to narrow down potential edge cases with identical VKs - # reading from cache at the same time. Create this folder up-front. - mkdir -p $tmp_dir/$contract_hash - - # Pipe each contract function, one per line (jq -c), into parallel calls of process_function. - # The returned jsons from process_function are converted back to a json array in the second jq -s call. - # When slurping (-s) in the last jq, we get an array of two elements: - # .[0] is the original json (at $json_path) - # .[1] is the updated functions on stdin (-) - # * merges their fields. - # Write each function to a separate temp file to avoid pipe/stdin issues with large JSON - local func_dir=$(mktemp -d -p $tmp_dir) - local i=0 - while IFS= read -r func_json; do - echo "$func_json" > "$func_dir/$i.json" - ((i++)) || true - done < <(jq -c '.functions[]' $json_path) - - # Process each function file in parallel - ls "$func_dir"/*.json | sort -V | \ - parallel $PARALLEL_FLAGS --keep-order 'cat {} | process_function '"$contract_hash" | \ - jq -s '{functions: .}' | jq -s '.[0] * {functions: .[1].functions}' $json_path - > $tmp_dir/$filename - mv $tmp_dir/$filename $json_path } export -f compile @@ -220,7 +129,6 @@ function build { if [ "$#" -eq 0 ]; then rm -rf target - mkdir -p $tmp_dir local contracts=$(grep -oP "(?<=$folder_name/)[^\"]+" Nargo.toml) else local contracts="$@" diff --git a/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/src/main.nr index 443cbca8a0f5..4d950b4cf661 100644 --- a/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/ecdsa_k_account_contract/src/main.nr @@ -12,7 +12,10 @@ pub contract EcdsaKAccount { storage::storage, }, messages::message_delivery::MessageDelivery, - oracle::{auth_witness::get_auth_witness, notes::{get_sender_for_tags, set_sender_for_tags}}, + oracle::{ + auth_witness::get_auth_witness_as_bytes, + notes::{get_sender_for_tags, set_sender_for_tags}, + }, state_vars::SinglePrivateImmutable, }; @@ -81,14 +84,9 @@ pub contract EcdsaKAccount { let storage = Storage::init(context); let public_key = storage.signing_public_key.get_note(); - // Load auth witness // Safety: The witness is only used as a "magical value" that makes the signature verification below pass. // Hence it's safe. - let witness: [Field; 64] = unsafe { get_auth_witness(outer_hash) }; - let mut signature: [u8; 64] = [0; 64]; - for i in 0..64 { - signature[i] = witness[i] as u8; - } + let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; // Verify payload signature using Ethereum's signing scheme // Note that noir expects the hash of the message/challenge as input to the ECDSA verification. diff --git a/noir-projects/noir-contracts/contracts/account/ecdsa_r_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/ecdsa_r_account_contract/src/main.nr index 610b3d4eaddc..69279ff5c129 100644 --- a/noir-projects/noir-contracts/contracts/account/ecdsa_r_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/ecdsa_r_account_contract/src/main.nr @@ -11,7 +11,10 @@ pub contract EcdsaRAccount { storage::storage, }, messages::message_delivery::MessageDelivery, - oracle::{auth_witness::get_auth_witness, notes::{get_sender_for_tags, set_sender_for_tags}}, + oracle::{ + auth_witness::get_auth_witness_as_bytes, + notes::{get_sender_for_tags, set_sender_for_tags}, + }, state_vars::SinglePrivateImmutable, }; @@ -79,14 +82,9 @@ pub contract EcdsaRAccount { let storage = Storage::init(context); let public_key = storage.signing_public_key.get_note(); - // Load auth witness // Safety: The witness is only used as a "magical value" that makes the signature verification below pass. // Hence it's safe. - let witness: [Field; 64] = unsafe { get_auth_witness(outer_hash) }; - let mut signature: [u8; 64] = [0; 64]; - for i in 0..64 { - signature[i] = witness[i] as u8; - } + let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; // Verify payload signature using Ethereum's signing scheme // Note that noir expects the hash of the message/challenge as input to the ECDSA verification. diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr index 34fd2a160d9f..ff55d41771a5 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr @@ -20,7 +20,7 @@ pub contract SchnorrAccount { }, messages::message_delivery::MessageDelivery, oracle::{ - auth_witness::get_auth_witness, + auth_witness::get_auth_witness_as_bytes, get_nullifier_membership_witness::get_low_nullifier_membership_witness, notes::{get_sender_for_tags, set_sender_for_tags}, }, @@ -95,14 +95,9 @@ pub contract SchnorrAccount { let storage = Storage::init(context); let public_key = storage.signing_public_key.get_note(); - // Load auth witness // Safety: The witness is only used as a "magical value" that makes the signature verification below pass. // Hence it's safe. - let witness: [Field; 64] = unsafe { get_auth_witness(outer_hash) }; - let mut signature: [u8; 64] = [0; 64]; - for i in 0..64 { - signature[i] = witness[i] as u8; - } + let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; let pub_key = std::embedded_curve_ops::EmbeddedCurvePoint { x: public_key.x, y: public_key.y }; @@ -128,11 +123,7 @@ pub contract SchnorrAccount { inner_hash, ); - let witness: [Field; 64] = get_auth_witness(message_hash); - let mut signature: [u8; 64] = [0; 64]; - for i in 0..64 { - signature[i] = witness[i] as u8; - } + let signature: [u8; 64] = get_auth_witness_as_bytes(message_hash); let pub_key = std::embedded_curve_ops::EmbeddedCurvePoint { x: public_key.x, y: public_key.y }; let valid_in_private = diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr index ac0d6e83f6f9..6657da79aeb1 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr @@ -7,7 +7,7 @@ pub contract SchnorrHardcodedAccount { authwit::{account::AccountActions, entrypoint::app::AppPayload}, context::PrivateContext, macros::functions::{allow_phase_change, external, view}, - oracle::{auth_witness::get_auth_witness, notes::set_sender_for_tags}, + oracle::{auth_witness::get_auth_witness_as_bytes, notes::set_sender_for_tags}, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -38,15 +38,9 @@ pub contract SchnorrHardcodedAccount { #[contract_library_method] fn is_valid_impl(_context: &mut PrivateContext, outer_hash: Field) -> bool { - // Load auth witness and format as an u8 array - // Safety: The witness is only used as a "magical value" that makes the signature verification below pass. // Hence it's safe. - let witness: [Field; 64] = unsafe { get_auth_witness(outer_hash) }; - let mut signature: [u8; 64] = [0; 64]; - for i in 0..64 { - signature[i] = witness[i] as u8; - } + let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; // Verify signature using hardcoded public key schnorr::verify_signature( diff --git a/noir-projects/noir-contracts/scripts/bootstrap_just_one_contract.sh b/noir-projects/noir-contracts/scripts/bootstrap_just_one_contract.sh index 2fad30ebac67..6681d0139a72 100755 --- a/noir-projects/noir-contracts/scripts/bootstrap_just_one_contract.sh +++ b/noir-projects/noir-contracts/scripts/bootstrap_just_one_contract.sh @@ -28,11 +28,7 @@ echo "Compiling contract..." NARGO=${NARGO:-../../../noir/noir-repo/target/release/nargo} $NARGO compile --silence-warnings --inliner-aggressiveness 0 --package $CONTRACT_PACKAGE_NAME -# Strip __aztec_nr_internals__ prefix from function names in the ABI. -echo "Stripping aztec nr prefix..." -./strip_aztec_nr_prefix.sh "../target/$JSON_NAME.json" - -# Transpile public functions and generate VKs for private functions. +# Transpile public functions, strip internal prefixes, and generate VKs for private functions. echo "Processing contract artifact..." BB=${BB:-../../../barretenberg/cpp/build/bin/bb} "$BB" aztec_process -i "../target/$JSON_NAME.json" -o "../target/$JSON_NAME.json" -f diff --git a/noir-projects/noir-contracts/scripts/strip_aztec_nr_prefix.sh b/noir-projects/noir-contracts/scripts/strip_aztec_nr_prefix.sh deleted file mode 100755 index e0466361421d..000000000000 --- a/noir-projects/noir-contracts/scripts/strip_aztec_nr_prefix.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# This script strips the `__aztec_nr_internals__` prefix from function names in the exported contract ABI JSON. -# -# Background: -# The #[aztec] macro generates new functions prefixed with `__aztec_nr_internals__` from the original external contract -# functions (see aztec.nr and internals_functions_generation/mod.nr). The original functions are then modified to be -# uncallable (replaced with static_assert(false, ...)) to prevent developers from inadvertently calling them directly -# instead of performing proper contract calls. -# -# Why this script is needed: -# During compilation, the transformed functions with the `__aztec_nr_internals__` prefix are what actually get -# compiled into circuits. However, in the exported ABI JSON that external tools and developers use, we want to -# expose the original function names without the internal prefix. This makes the ABI cleaner and matches what -# developers originally wrote in their contracts. - -json_path=$1 -temp_file="${json_path}.tmp" - -jq '.functions |= map(.name |= sub("^__aztec_nr_internals__"; ""))' "$json_path" > "$temp_file" -mv "$temp_file" "$json_path" diff --git a/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md b/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md index d1ec0be540d5..760425a0bcae 100644 --- a/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md +++ b/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md @@ -207,11 +207,9 @@ for pkg in side_effect_contract parent_contract; do --silence-warnings --inliner-aggressiveness 0 --package ${pkg} /usr/src/barretenberg/cpp/build/bin/bb-avm aztec_process \ -i target/${artifact}.json - jq '.functions |= map(.name |= sub(\"^__aztec_nr_internals__\"; \"\"))' \ - target/${artifact}.json > /tmp/${artifact}.json " - docker cp "aztec-sandbox-nightly:/tmp/${artifact}.json" \ + docker cp "aztec-sandbox-nightly:/tmp/nightly-build/target/${artifact}.json" \ "contracts/target/${artifact}.json" done ``` @@ -291,7 +289,7 @@ Run `bb-avm aztec_process` (step 4b). Run `bb-avm aztec_process` (step 4b) -- it generates both transpiled bytecode and VKs. ### "Constructor method initialize not found" -The `__aztec_nr_internals__` prefix wasn't stripped. Run the `jq` step in 4b. +The internal prefix wasn't stripped. Ensure `bb-avm aztec_process` ran successfully in step 4b. ### Wallet "inquirer not found" error Run step 3. @@ -326,8 +324,7 @@ on the first request. The Rust fuzzer resolves aliases (`accounts:test0`, ### Build pipeline 1. `nargo compile` -- raw artifact JSON with `__aztec_nr_internals__` prefixed names -2. `bb-avm aztec_process` -- transpiles public bytecode to AVM + generates private VKs -3. `jq` strip prefix -- removes `__aztec_nr_internals__` from function names +2. `bb-avm aztec_process` -- transpiles public bytecode to AVM, strips internal prefixes, and generates private VKs ### Version matrix (as of 2026-02-25) diff --git a/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh b/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh index 7fa98441b9f3..207566171bcf 100755 --- a/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh +++ b/noir-projects/protocol-fuzzer/setup-nightly-sandbox.sh @@ -208,11 +208,9 @@ for contract_pkg in side_effect_contract parent_contract; do --silence-warnings --inliner-aggressiveness 0 --package ${contract_pkg} /usr/src/barretenberg/cpp/build/bin/bb-avm aztec_process \ -i target/${artifact}.json - jq '.functions |= map(.name |= sub(\"^__aztec_nr_internals__\"; \"\"))' \ - target/${artifact}.json > /tmp/${artifact}.json " - docker cp "${CONTAINER_NAME}:/tmp/${artifact}.json" \ + docker cp "${CONTAINER_NAME}:/tmp/nightly-build/target/${artifact}.json" \ "${CONTRACTS_DIR}/target/${artifact}.json" log "Artifact copied to contracts/target/${artifact}.json" done diff --git a/noir-projects/scripts/test_aztec_process.sh b/noir-projects/scripts/test_aztec_process.sh index 83a04c3fbba1..da4aecb4df36 100755 --- a/noir-projects/scripts/test_aztec_process.sh +++ b/noir-projects/scripts/test_aztec_process.sh @@ -93,4 +93,14 @@ fi echo "✓ Force regeneration works" +# Test 4: Verify prefix stripping +echo "Test 4: Prefix stripping" +prefix_count=$(jq '[.functions[] | select(.name | startswith("__aztec_nr_internals__"))] | length' "$tmp_output") +if [ "$prefix_count" -ne 0 ]; then + echo "Error: Found $prefix_count functions with __aztec_nr_internals__ prefix after processing" + exit 1 +fi + +echo "✓ Prefix stripping works (no functions with internal prefix)" + echo "All bb aztec_process tests passed!" diff --git a/yarn-project/aztec/src/cli/aztec_start_action.ts b/yarn-project/aztec/src/cli/aztec_start_action.ts index 7a3c3331a4a4..7b03c921e83e 100644 --- a/yarn-project/aztec/src/cli/aztec_start_action.ts +++ b/yarn-project/aztec/src/cli/aztec_start_action.ts @@ -27,8 +27,7 @@ export async function aztecStart(options: any, userLog: LogFn, debugLogger: Logg let config: ChainConfig | undefined = undefined; if (options.localNetwork) { - const localNetwork = extractNamespacedOptions(options, 'local-network'); - localNetwork.testAccounts = true; + const localNetwork = extractNamespacedOptions(options, 'localNetwork'); userLog(`${splash}\n${github}\n\n`); userLog(`Setting up Aztec local network ${packageVersion ?? 'unknown'}, please stand by...`); diff --git a/yarn-project/aztec/src/cli/aztec_start_options.test.ts b/yarn-project/aztec/src/cli/aztec_start_options.test.ts index 057e459467bc..befe8bedf91a 100644 --- a/yarn-project/aztec/src/cli/aztec_start_options.test.ts +++ b/yarn-project/aztec/src/cli/aztec_start_options.test.ts @@ -94,6 +94,21 @@ describe('aztec_start_options commander integration', () => { expect(typeof opts.port).toBe('number'); }); + it('respects TEST_ACCOUNTS env var for local network', () => { + process.env.TEST_ACCOUNTS = 'false'; + const cmd = buildCommandWith(['LOCAL_NETWORK']); + cmd.parse(['node', 'cli']); + const opts = cmd.opts(); + expect(opts['localNetwork.testAccounts']).toBe(false); + }); + + it('defaults testAccounts to true for local network', () => { + const cmd = buildCommandWith(['LOCAL_NETWORK']); + cmd.parse(['node', 'cli']); + const opts = cmd.opts(); + expect(opts['localNetwork.testAccounts']).toBe(true); + }); + it('parses optional boolean flag values', () => { const cmd = buildCommandWith(['P2P SUBSYSTEM']); diff --git a/yarn-project/aztec/src/cli/aztec_start_options.ts b/yarn-project/aztec/src/cli/aztec_start_options.ts index 3034ceea504c..b4c85349f9cb 100644 --- a/yarn-project/aztec/src/cli/aztec_start_options.ts +++ b/yarn-project/aztec/src/cli/aztec_start_options.ts @@ -129,6 +129,12 @@ export const aztecStartOptions: { [key: string]: AztecStartOption[] } = { defaultValue: DefaultMnemonic, env: 'MNEMONIC', }, + { + flag: '--local-network.testAccounts', + description: 'Deploy test accounts on local network start', + env: 'TEST_ACCOUNTS', + ...booleanConfigHelper(true), + }, ], API: [ { diff --git a/yarn-project/aztec/src/cli/cmds/compile.ts b/yarn-project/aztec/src/cli/cmds/compile.ts index 8c9464c11791..5fc2259b2305 100644 --- a/yarn-project/aztec/src/cli/cmds/compile.ts +++ b/yarn-project/aztec/src/cli/cmds/compile.ts @@ -3,7 +3,7 @@ import type { LogFn } from '@aztec/foundation/log'; import { execFileSync } from 'child_process'; import type { Command } from 'commander'; -import { readFile, writeFile } from 'fs/promises'; +import { readFile } from 'fs/promises'; import { join } from 'path'; import { readArtifactFiles } from './utils/artifacts.js'; @@ -25,19 +25,6 @@ async function collectContractArtifacts(): Promise { return files.filter(f => Array.isArray(f.content.functions)).map(f => f.filePath); } -/** Strips the `__aztec_nr_internals__` prefix from function names in contract artifacts. */ -async function stripInternalPrefixes(artifactPaths: string[]): Promise { - for (const path of artifactPaths) { - const artifact = JSON.parse(await readFile(path, 'utf-8')); - for (const fn of artifact.functions) { - if (typeof fn.name === 'string') { - fn.name = fn.name.replace(/^__aztec_nr_internals__/, ''); - } - } - await writeFile(path, JSON.stringify(artifact, null, 2) + '\n'); - } -} - /** Returns the set of package names that are contract crates in the current workspace. */ async function getContractPackageNames(): Promise> { const contractNames = new Set(); @@ -161,9 +148,6 @@ async function compileAztecContract(nargoArgs: string[], log: LogFn): Promise ['-i', a]); await run(bb, ['aztec_process', ...bbArgs]); - - // TODO: This should be part of bb aztec_process! - await stripInternalPrefixes(artifacts); } log('Compilation complete!'); diff --git a/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.test.ts b/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.test.ts index 490b1281bfe1..544e201bfc0d 100644 --- a/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.test.ts +++ b/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.test.ts @@ -67,6 +67,75 @@ describe('warnIfAztecVersionMismatch', () => { expect(logMessages[0]).toContain('v1.0.0'); }); + it('warns when a non-aztec aztec-nr dependency tag does not match the CLI version', async () => { + await makePackage(tempDir, 'project', 'contract', { + // eslint-disable-next-line camelcase + uint_note: '{ git = "https://github.com/AztecProtocol/aztec-nr", tag = "v0.99.0", directory = "uint-note" }', + }); + + await warnIfAztecVersionMismatch(log, '1.0.0'); + + expect(logMessages).toHaveLength(1); + expect(logMessages[0]).toContain('WARNING'); + expect(logMessages[0]).toContain('uint_note'); + expect(logMessages[0]).toContain('v0.99.0'); + expect(logMessages[0]).toContain('v1.0.0'); + }); + + it('warns about a sibling aztec-nr dependency even when the aztec dependency matches', async () => { + await makePackage(tempDir, 'project', 'contract', { + aztec: '{ git = "https://github.com/AztecProtocol/aztec-nr", tag = "v1.0.0", directory = "aztec" }', + // eslint-disable-next-line camelcase + uint_note: '{ git = "https://github.com/AztecProtocol/aztec-nr", tag = "v0.99.0", directory = "uint-note" }', + }); + + await warnIfAztecVersionMismatch(log, '1.0.0'); + + expect(logMessages).toHaveLength(1); + expect(logMessages[0]).toContain('WARNING'); + expect(logMessages[0]).toContain('uint_note'); + expect(logMessages[0]).not.toMatch(/—\s*aztec\s*\(/); + }); + + it('does not warn when multiple aztec-nr dependencies all match the CLI version', async () => { + await makePackage(tempDir, 'project', 'contract', { + aztec: '{ git = "https://github.com/AztecProtocol/aztec-nr", tag = "v1.0.0", directory = "aztec" }', + // eslint-disable-next-line camelcase + uint_note: '{ git = "https://github.com/AztecProtocol/aztec-nr", tag = "v1.0.0", directory = "uint-note" }', + // eslint-disable-next-line camelcase + compressed_string: + '{ git = "https://github.com/AztecProtocol/aztec-nr", tag = "v1.0.0", directory = "compressed-string" }', + }); + + await warnIfAztecVersionMismatch(log, '1.0.0'); + + expect(logMessages.filter(m => m.includes('WARNING'))).toHaveLength(0); + }); + + it('does not warn for unrelated third-party git dependencies', async () => { + await makePackage(tempDir, 'project', 'contract', { + aztec: '{ git = "https://github.com/AztecProtocol/aztec-nr", tag = "v1.0.0", directory = "aztec" }', + // eslint-disable-next-line camelcase + noir_string_search: '{ git = "https://github.com/noir-lang/noir_string_search", tag = "v0.1.0" }', + }); + + await warnIfAztecVersionMismatch(log, '1.0.0'); + + expect(logMessages.filter(m => m.includes('WARNING'))).toHaveLength(0); + }); + + it('normalizes trailing slashes and .git suffixes in the aztec-nr git URL', async () => { + await makePackage(tempDir, 'project', 'contract', { + aztec: '{ git = "https://github.com/AztecProtocol/aztec-nr.git", tag = "v1.0.0", directory = "aztec" }', + // eslint-disable-next-line camelcase + uint_note: '{ git = "https://github.com/AztecProtocol/aztec-nr/", tag = "v1.0.0", directory = "uint-note" }', + }); + + await warnIfAztecVersionMismatch(log, '1.0.0'); + + expect(logMessages.filter(m => m.includes('WARNING'))).toHaveLength(0); + }); + it('warns when the CLI version is not available', async () => { await makePackage(tempDir, 'project', 'contract'); diff --git a/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.ts b/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.ts index 58c68c1ced90..3cedfa39a660 100644 --- a/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.ts +++ b/yarn-project/aztec/src/cli/cmds/utils/warn_if_aztec_version_mismatch.ts @@ -7,7 +7,25 @@ import { join } from 'path'; import { collectCrateDirs } from './collect_crate_dirs.js'; -/** Warns if the `aztec` dependency tag in any crate's Nargo.toml doesn't match the CLI version. */ +/** Returns true if the given git URL points to the AztecProtocol/aztec-nr repository. */ +function isAztecNrGitUrl(gitUrl: string): boolean { + let url: URL; + try { + url = new URL(gitUrl); + } catch { + return false; + } + if (url.hostname !== 'github.com') { + return false; + } + const repoPath = url.pathname + .replace(/^\//, '') + .replace(/\.git$/, '') + .replace(/\/$/, ''); + return repoPath === 'AztecProtocol/aztec-nr'; +} + +/** Warns if any aztec-nr git dependency in a crate's Nargo.toml has a tag that doesn't match the CLI version. */ export async function warnIfAztecVersionMismatch(log: LogFn, cliVersion?: string): Promise { const version = cliVersion ?? getPackageVersion(); if (!version) { @@ -16,7 +34,7 @@ export async function warnIfAztecVersionMismatch(log: LogFn, cliVersion?: string } const expectedTag = `v${version}`; - const mismatches: { file: string; tag: string }[] = []; + const mismatches: { file: string; depName: string; tag: string }[] = []; const crateDirs = await collectCrateDirs('.', { skipGitDeps: true }); @@ -30,23 +48,28 @@ export async function warnIfAztecVersionMismatch(log: LogFn, cliVersion?: string } const parsed = TOML.parse(content) as Record; - const aztecDep = (parsed.dependencies as Record)?.aztec; - if (!aztecDep || typeof aztecDep !== 'object' || typeof aztecDep.tag !== 'string') { - // If a dep called "aztec" doesn't exist or it does not get parsed to an object or it doesn't have a tag defined - // we skip the check. - continue; - } + const deps = (parsed.dependencies as Record) ?? {}; - if (aztecDep.tag !== expectedTag) { - mismatches.push({ file: tomlPath, tag: aztecDep.tag }); + for (const [depName, dep] of Object.entries(deps)) { + // Skip non-object deps (e.g. malformed entries) and anything that isn't a tagged git dep. + if (!dep || typeof dep !== 'object' || typeof dep.git !== 'string' || typeof dep.tag !== 'string') { + continue; + } + // Only flag deps that are sourced from the aztec-nr repo. + if (!isAztecNrGitUrl(dep.git)) { + continue; + } + if (dep.tag !== expectedTag) { + mismatches.push({ file: tomlPath, depName, tag: dep.tag }); + } } } if (mismatches.length > 0) { - const details = mismatches.map(m => ` ${m.file} (${m.tag})`).join('\n'); + const details = mismatches.map(m => ` ${m.file} — ${m.depName} (${m.tag})`).join('\n'); log( `WARNING: Aztec dependency version mismatch detected.\n` + - `The following crates have an aztec dependency that does not match the CLI version (${expectedTag}):\n` + + `The following aztec-nr dependencies do not match the CLI version (${expectedTag}):\n` + `${details}\n\n` + `See https://docs.aztec.network/errors/9 for how to update your dependencies.`, ); diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index d7f77efab6e5..c052adf3e163 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -116,6 +116,43 @@ describe('BlockSynchronizer', () => { expect(rollback).toHaveBeenCalledWith(3, 4); }); + describe('stop', () => { + it('resolves immediately when no sync is in progress', async () => { + await synchronizer.stop(); + expect(blockStream.stop).toHaveBeenCalled(); + }); + + it('waits for in-progress sync to complete', async () => { + let resolveSync!: () => void; + const syncBlocker = new Promise(resolve => { + resolveSync = resolve; + }); + blockStream.sync.mockReturnValue(syncBlocker); + aztecNode.getBlockHeader.mockResolvedValue((await L2Block.random(BlockNumber(0))).header); + + // Start a sync (don't await) + const syncPromise = synchronizer.sync(); + + // stop() should not resolve until the sync finishes + let stopped = false; + const stopPromise = synchronizer.stop().then(() => { + stopped = true; + }); + + // Give the event loop a tick + await new Promise(resolve => setTimeout(resolve, 10)); + expect(stopped).toBe(false); + + // Release the sync + resolveSync(); + await syncPromise; + await stopPromise; + + expect(stopped).toBe(true); + expect(blockStream.stop).toHaveBeenCalled(); + }); + }); + describe('syncChainTip config', () => { it('updates anchor on blocks-added when syncChainTip is proposed (default)', async () => { synchronizer = createSynchronizer({ syncChainTip: 'proposed' }); diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts index 2ea826e439aa..ea2f889d529e 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts @@ -1,5 +1,6 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; +import { SerialQueue } from '@aztec/foundation/queue'; import type { AztecAsyncKVStore } from '@aztec/kv-store'; import type { L2TipsKVStore } from '@aztec/kv-store/stores'; import { BlockHash, L2BlockStream, type L2BlockStreamEvent, type L2BlockStreamEventHandler } from '@aztec/stdlib/block'; @@ -20,6 +21,7 @@ import type { PrivateEventStore } from '../storage/private_event_store/private_e export class BlockSynchronizer implements L2BlockStreamEventHandler { private log: Logger; private isSyncing: Promise | undefined; + private readonly eventQueue = new SerialQueue(); protected readonly blockStream: L2BlockStream; constructor( @@ -35,6 +37,7 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler { ) { this.log = createLogger('pxe:block_synchronizer', bindings); this.blockStream = this.createBlockStream(config); + this.eventQueue.start(); } protected createBlockStream(config: Partial): L2BlockStream { @@ -52,8 +55,12 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler { ); } - /** Handle events emitted by the block stream. */ - public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { + /** Handle events emitted by the block stream. Serialized to prevent concurrent mutations to anchor state. */ + public handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { + return this.eventQueue.put(() => this.doHandleBlockStreamEvent(event)); + } + + private async doHandleBlockStreamEvent(event: L2BlockStreamEvent): Promise { await this.l2TipsStore.handleBlockStreamEvent(event); switch (event.type) { @@ -167,6 +174,13 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler { } } + /** Stops the block synchronizer, waiting for any in-progress sync and queued events to complete. */ + public async stop() { + await this.isSyncing; + await this.blockStream.stop(); + await this.eventQueue.end(); + } + private async doSync() { let currentHeader; diff --git a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts index cd164f66ee47..8f56af4e8325 100644 --- a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts +++ b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts @@ -208,7 +208,7 @@ export class ContractFunctionSimulator { } if (request.origin !== contractAddress) { - this.log.warn( + throw new Error( `Request origin does not match contract address in simulation. Request origin: ${request.origin}, contract address: ${contractAddress}`, ); } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 5081637d6d5c..f88bbce7aa71 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -486,6 +486,40 @@ describe('Private Execution test suite', () => { }); }); + it('throws when request origin does not match contract address', async () => { + const contractAddress = await mockContractInstance(TestContractArtifact); + const differentAddress = await AztecAddress.random(); + contracts[differentAddress.toString()] = TestContractArtifact; + + const functionArtifact = getFunctionArtifactByName(TestContractArtifact, 'emit_array_as_encrypted_log'); + const selector = await FunctionSelector.fromNameAndParameters(functionArtifact.name, functionArtifact.parameters); + const hashedArguments = await HashedValues.fromArgs( + encodeArguments(functionArtifact, [Fr.ZERO, times(5, () => Fr.random()), owner, false]), + ); + + const txRequest = TxExecutionRequest.from({ + origin: differentAddress, + firstCallArgsHash: hashedArguments.hash, + functionSelector: selector, + txContext: TxContext.from(txContextFields), + argsOfCalls: [hashedArguments], + authWitnesses: [], + capsules: [], + salt: Fr.random(), + }); + + await expect( + acirSimulator.run(txRequest, { + contractAddress, + selector, + anchorBlockHeader, + senderForTags, + jobId: TEST_JOB_ID, + scopes: [owner], + }), + ).rejects.toThrow('Request origin does not match contract address'); + }); + describe('stateful test contract', () => { let contractAddress: AztecAddress; const mockFirstNullifier = new Fr(1111); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index de436341524e..e3354178f9dd 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -81,7 +81,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP private readonly taggingIndexCache: ExecutionTaggingIndexCache; private readonly senderTaggingStore: SenderTaggingStore; private totalPublicCalldataCount: number; - protected sideEffectCounter: number; + private readonly initialSideEffectCounter: number; private senderForTags?: AztecAddress; private readonly simulator?: CircuitSimulator; @@ -100,13 +100,18 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP this.taggingIndexCache = args.taggingIndexCache; this.senderTaggingStore = args.senderTaggingStore; this.totalPublicCalldataCount = args.totalPublicCalldataCount ?? 0; - this.sideEffectCounter = args.sideEffectCounter ?? 0; + this.initialSideEffectCounter = args.sideEffectCounter ?? 0; this.senderForTags = args.senderForTags; this.simulator = args.simulator; } public getPrivateContextInputs(): PrivateContextInputs { - return new PrivateContextInputs(this.callContext, this.anchorBlockHeader, this.txContext, this.sideEffectCounter); + return new PrivateContextInputs( + this.callContext, + this.anchorBlockHeader, + this.txContext, + this.initialSideEffectCounter, + ); } // We still need this function until we can get user-defined ordering of structs for fn arguments diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index a50e1e4f01c9..450903a01f5f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -333,10 +333,9 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } /** - * Returns an auth witness for the given message hash. Checks on the list of transient witnesses - * for this transaction first, and falls back to the local database if not found. + * Returns an auth witness for the given message hash from the list of transient witnesses for this transaction. * @param messageHash - Hash of the message to authenticate. - * @returns Authentication witness for the requested message hash. + * @returns Authentication witness for the requested message hash, or undefined if not found. */ public getAuthWitness(messageHash: Fr): Promise { return Promise.resolve(this.authWitnesses.find(w => w.requestHash.equals(messageHash))?.witness); diff --git a/yarn-project/pxe/src/contract_function_simulator/pick_notes.test.ts b/yarn-project/pxe/src/contract_function_simulator/pick_notes.test.ts index a42cdfd3470a..90731bb38725 100644 --- a/yarn-project/pxe/src/contract_function_simulator/pick_notes.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/pick_notes.test.ts @@ -376,4 +376,43 @@ describe('getNotes', () => { expect(() => pickNotes(notes, options)).toThrow('Invalid comparator value: 99'); }); + + it('throws when selector.index is out of bounds', () => { + const notes = [createNote([1n, 2n])]; + const options = { + selects: [ + { + selector: { index: 5, offset: 0, length: 32 }, + value: new Fr(1n), + comparator: Comparator.EQ, + }, + ], + }; + + expect(() => pickNotes(notes, options)).toThrow(/index 5 out of bounds/); + }); + + it('throws when selector.index is out of bounds in a sort', () => { + const notes = [createNote([1n]), createNote([2n])]; + const options = { + sorts: [{ selector: { index: 3, offset: 0, length: 32 }, order: SortOrder.ASC }], + }; + + expect(() => pickNotes(notes, options)).toThrow(/index 3 out of bounds/); + }); + + it('throws when selector.offset + selector.length exceeds Fr buffer size', () => { + const notes = [createNote([1n])]; + const options = { + selects: [ + { + selector: { index: 0, offset: 30, length: 5 }, + value: new Fr(0n), + comparator: Comparator.EQ, + }, + ], + }; + + expect(() => pickNotes(notes, options)).toThrow(/exceeds Fr buffer size/); + }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/pick_notes.ts b/yarn-project/pxe/src/contract_function_simulator/pick_notes.ts index 46880d727173..82c4d82386a3 100644 --- a/yarn-project/pxe/src/contract_function_simulator/pick_notes.ts +++ b/yarn-project/pxe/src/contract_function_simulator/pick_notes.ts @@ -85,6 +85,14 @@ interface ContainsNote { } const selectPropertyFromPackedNoteContent = (noteData: Fr[], selector: PropertySelector): Fr => { + if (selector.index >= noteData.length) { + throw new Error(`Property selector index ${selector.index} out of bounds for note with ${noteData.length} fields`); + } + if (selector.offset + selector.length > Fr.SIZE_IN_BYTES) { + throw new Error( + `Property selector range (offset=${selector.offset}, length=${selector.length}) exceeds Fr buffer size of ${Fr.SIZE_IN_BYTES} bytes`, + ); + } const noteValueBuffer = noteData[selector.index].toBuffer(); // Noir's PropertySelector counts offset from the LSB (last byte of the big-endian buffer), // so offset=0,length=Fr.SIZE_IN_BYTES reads the entire field, and offset=0,length=1 reads the last byte. diff --git a/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts b/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts index 6bbf8983af94..52e3ceb10327 100644 --- a/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts +++ b/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts @@ -7,13 +7,13 @@ import { getVKIndex, getVKSiblingPath } from '@aztec/noir-protocol-circuits-type import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import type { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { BlockHash } from '@aztec/stdlib/block'; import { type ContractInstanceWithAddress, computeSaltedInitializationHash } from '@aztec/stdlib/contract'; import { DelayedPublicMutableValues, DelayedPublicMutableValuesWithHash } from '@aztec/stdlib/delayed-public-mutable'; import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { UpdatedClassIdHints } from '@aztec/stdlib/kernel'; import type { NullifierMembershipWitness } from '@aztec/stdlib/trees'; +import type { BlockHeader } from '@aztec/stdlib/tx'; import type { VerificationKeyAsFields } from '@aztec/stdlib/vks'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; @@ -26,7 +26,7 @@ export class PrivateKernelOracle { private contractStore: ContractStore, private keyStore: KeyStore, private node: AztecNode, - private blockHash: BlockHash, + private blockHeader: BlockHeader, ) {} /** Retrieves the preimage of a contract address from the registered contract instances db. */ @@ -80,22 +80,20 @@ export class PrivateKernelOracle { } /** Returns a membership witness with the sibling path and leaf index in our note hash tree. */ - getNoteHashMembershipWitness(noteHash: Fr): Promise | undefined> { - return this.node.getNoteHashMembershipWitness(this.blockHash, noteHash); + async getNoteHashMembershipWitness( + noteHash: Fr, + ): Promise | undefined> { + return this.node.getNoteHashMembershipWitness(await this.blockHeader.hash(), noteHash); } /** Returns a membership witness with the sibling path and leaf index in our nullifier indexed merkle tree. */ - getNullifierMembershipWitness(nullifier: Fr): Promise { - return this.node.getNullifierMembershipWitness(this.blockHash, nullifier); + async getNullifierMembershipWitness(nullifier: Fr): Promise { + return this.node.getNullifierMembershipWitness(await this.blockHeader.hash(), nullifier); } /** Returns the root of our note hash merkle tree. */ - async getNoteHashTreeRoot(): Promise { - const header = await this.node.getBlockHeader(this.blockHash); - if (!header) { - throw new Error(`No block header found for block hash ${this.blockHash}`); - } - return header.state.partial.noteHashTree.root; + getNoteHashTreeRoot(): Fr { + return this.blockHeader.state.partial.noteHashTree.root; } /** @@ -126,14 +124,16 @@ export class PrivateKernelOracle { ProtocolContractAddress.ContractInstanceRegistry, delayedPublicMutableHashSlot, ); - const updatedClassIdWitness = await this.node.getPublicDataWitness(this.blockHash, hashLeafSlot); + const blockHash = await this.blockHeader.hash(); + + const updatedClassIdWitness = await this.node.getPublicDataWitness(blockHash, hashLeafSlot); if (!updatedClassIdWitness) { throw new Error(`No public data tree witness found for ${hashLeafSlot}`); } const readStorage = (storageSlot: Fr) => - this.node.getPublicStorageAt(this.blockHash, ProtocolContractAddress.ContractInstanceRegistry, storageSlot); + this.node.getPublicStorageAt(blockHash, ProtocolContractAddress.ContractInstanceRegistry, storageSlot); const delayedPublicMutableValues = await DelayedPublicMutableValues.readFromTree( delayedPublicMutableSlot, readStorage, diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 164d64af2bd9..7681edc72d03 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -486,8 +486,7 @@ export class PXE { config: PrivateKernelExecutionProverConfig, ): Promise> { const anchorBlockHeader = await this.anchorBlockStore.getBlockHeader(); - const anchorBlockHash = await anchorBlockHeader.hash(); - const kernelOracle = new PrivateKernelOracle(this.contractStore, this.keyStore, this.node, anchorBlockHash); + const kernelOracle = new PrivateKernelOracle(this.contractStore, this.keyStore, this.node, anchorBlockHeader); const kernelTraceProver = new PrivateKernelExecutionProver( kernelOracle, proofCreator, @@ -581,8 +580,8 @@ export class PXE { if (wasAdded) { this.log.info(`Added sender:\n ${sender.toString()}`); // Wipe the entire sync cache: the new sender's tagged logs could contain notes/events for any contract, so - // all contracts must re-sync to discover them. - this.contractSyncService.wipe(); + // all contracts must re-sync to discover them. Queued to avoid wiping while a job is in flight. + await this.#putInJobQueue(() => Promise.resolve(this.contractSyncService.wipe())); } else { this.log.info(`Sender:\n "${sender.toString()}"\n already registered.`); } @@ -1180,6 +1179,7 @@ export class PXE { */ public async stop(): Promise { await this.jobQueue.end(); + await this.blockStateSynchronizer.stop(); await this.db.close(); } } diff --git a/yarn-project/pxe/src/storage/private_event_store/private_event_store.test.ts b/yarn-project/pxe/src/storage/private_event_store/private_event_store.test.ts index cfdcea692655..9bbdb98729f3 100644 --- a/yarn-project/pxe/src/storage/private_event_store/private_event_store.test.ts +++ b/yarn-project/pxe/src/storage/private_event_store/private_event_store.test.ts @@ -669,7 +669,7 @@ describe('PrivateEventStore', () => { txIndexInBlock: 0, eventIndexInTx: 0, }, - 'test', + 'before-rollback', ); await privateEventStore.commit('before-rollback'); @@ -747,6 +747,33 @@ describe('PrivateEventStore', () => { expect(events.length).toBe(1); expect(events[0].packedEvent).toEqual(msgContent1); }); + + it('throws when rollback is called while jobs are running', async () => { + await privateEventStore.storePrivateEventLog( + eventSelector, + randomness, + msgContent1, + Fr.random(), + { + contractAddress, + scope, + txHash: TxHash.random(), + l2BlockNumber: BlockNumber(100), + l2BlockHash, + txIndexInBlock: 0, + eventIndexInTx: 0, + }, + 'uncommitted-job', + ); + + await expect(privateEventStore.rollback(0, 10)).rejects.toThrow( + 'PXE private event store rollback is not allowed while jobs are running', + ); + + await privateEventStore.discardStaged('uncommitted-job'); + + await expect(privateEventStore.rollback(0, 10)).resolves.not.toThrow(); + }); }); describe('staging', () => { diff --git a/yarn-project/pxe/src/storage/private_event_store/private_event_store.ts b/yarn-project/pxe/src/storage/private_event_store/private_event_store.ts index 804178b882af..3518a7205abb 100644 --- a/yarn-project/pxe/src/storage/private_event_store/private_event_store.ts +++ b/yarn-project/pxe/src/storage/private_event_store/private_event_store.ts @@ -234,6 +234,10 @@ export class PrivateEventStore implements StagedStore { * IMPORTANT: This method must be called within a transaction to ensure atomicity. */ public async rollback(blockNumber: number, synchedBlockNumber: number): Promise { + if (this.#eventsForJob.size > 0) { + throw new Error('PXE private event store rollback is not allowed while jobs are running'); + } + // First pass: collect all event IDs for all blocks, starting reads during iteration to keep tx alive. const eventsByBlock: Map }[]> = new Map(); diff --git a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts index 1216b02724cd..feed4773fa54 100644 --- a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts +++ b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts @@ -113,7 +113,9 @@ export async function loadPrivateLogsForSenderRecipientPair( if (highestAgedIndex !== undefined && highestAgedIndex > highestFinalizedIndex) { // This is just a sanity check as this should never happen. - throw new Error('Highest aged index lower than highest finalized index invariant violated'); + throw new Error( + `Highest aged index (${highestAgedIndex}) must not exceed highest finalized index (${highestFinalizedIndex})`, + ); } await taggingStore.updateHighestFinalizedIndex(secret, highestFinalizedIndex, jobId);