From ae7cfe72b5c528fb533040c6da62c9b21f542f8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Rodr=C3=ADguez?= <sirasistant@gmail.com>
Date: Thu, 7 Nov 2024 15:50:24 +0100
Subject: [PATCH] feat: Constrain App function VKs (#9756)

Resolves https://github.com/AztecProtocol/aztec-packages/issues/9592
 - Now contract artifacts must have VKs in their private functions
- aztec-nargo inserts the verification keys after public function
transpilation
 - We no longer derive any VK in the TX proving flow
 - App VKs are now constrained in the private kernels
 - Bootstrap generates VKs for all apps (with s3 caching)
- PXE is currently accepting any VK present in the artifact as valid: we
should explore the correct interface for this in the future and wether
PXE can use those VKs without rederiving them from ACIR
---
 avm-transpiler/Dockerfile                     |   1 -
 avm-transpiler/Earthfile                      |   1 -
 .../scripts/compile_then_transpile.sh         |  31 ----
 aztec-nargo/Dockerfile                        |  11 +-
 aztec-nargo/Earthfile                         |   9 +-
 aztec-nargo/compile_then_postprocess.sh       |  49 ++++++
 .../barretenberg/dsl/acir_proofs/c_bind.cpp   |   9 +
 .../barretenberg/dsl/acir_proofs/c_bind.hpp   |   4 +-
 barretenberg/exports.json                     |  16 ++
 barretenberg/ts/src/barretenberg_api/index.ts |  24 +++
 boxes/Dockerfile                              |  12 +-
 boxes/Earthfile                               |   9 +-
 build_manifest.yml                            |   9 +-
 noir-projects/noir-contracts/bootstrap.sh     |  21 +--
 .../scripts/postprocess_contract.js           | 104 +++++++++++
 .../validate_contract_address.nr              |  11 +-
 .../validate_contract_address.nr              |  27 ++-
 .../crates/types/src/address/aztec_address.nr |   4 +-
 .../crates/types/src/hash.nr                  |  10 --
 .../crates/types/src/tests/fixture_builder.nr |  22 ++-
 .../src/tests/fixtures/contract_functions.nr  |   8 +-
 noir-projects/scripts/generate_vk_json.js     | 165 ++----------------
 noir-projects/scripts/verification_keys.js    | 104 +++++++++++
 .../src/deployment/broadcast_function.ts      |   2 +-
 .../verification_key/verification_key_data.ts |   4 +-
 .../src/contract/contract_class.ts            |  15 +-
 .../private_function_membership_proof.test.ts |   2 +-
 yarn-project/circuits.js/src/hash/hash.ts     |  22 +--
 yarn-project/foundation/src/abi/abi.ts        |   2 +-
 yarn-project/foundation/src/crypto/index.ts   |   1 +
 .../foundation/src/crypto/keys/index.ts       |   9 +
 .../noir-protocol-circuits-types/src/index.ts |   1 -
 .../src/scripts/generate_vk_hashes.ts         |   5 +-
 .../src/utils/vk_json.ts                      |   5 -
 .../src/protocol_contract_data.ts             |  14 +-
 .../src/scripts/generate_data.ts              |  17 --
 .../pxe/src/kernel_prover/kernel_prover.ts    |  10 +-
 .../simulator/src/client/private_execution.ts |   2 +-
 .../types/src/abi/contract_artifact.ts        |   5 +-
 39 files changed, 464 insertions(+), 313 deletions(-)
 delete mode 100755 avm-transpiler/scripts/compile_then_transpile.sh
 create mode 100755 aztec-nargo/compile_then_postprocess.sh
 create mode 100644 noir-projects/noir-contracts/scripts/postprocess_contract.js
 create mode 100644 noir-projects/scripts/verification_keys.js
 create mode 100644 yarn-project/foundation/src/crypto/keys/index.ts

diff --git a/avm-transpiler/Dockerfile b/avm-transpiler/Dockerfile
index d695622a009..3d26426c478 100644
--- a/avm-transpiler/Dockerfile
+++ b/avm-transpiler/Dockerfile
@@ -9,6 +9,5 @@ RUN apt-get update && apt-get install -y git
 RUN ./scripts/bootstrap_native.sh
 
 FROM ubuntu:noble
-COPY --from=0 /usr/src/avm-transpiler/scripts/compile_then_transpile.sh /usr/src/avm-transpiler/scripts/compile_then_transpile.sh
 COPY --from=0 /usr/src/avm-transpiler/target/release/avm-transpiler /usr/src/avm-transpiler/target/release/avm-transpiler
 ENTRYPOINT ["/usr/src/avm-transpiler/target/release/avm-transpiler"]
diff --git a/avm-transpiler/Earthfile b/avm-transpiler/Earthfile
index e4db70ea091..fc7ed7e5feb 100644
--- a/avm-transpiler/Earthfile
+++ b/avm-transpiler/Earthfile
@@ -18,7 +18,6 @@ build:
         --command="./scripts/bootstrap_native.sh && rm -rf target/release/{build,deps}" \
         --build_artifacts="target"
     SAVE ARTIFACT target/release/avm-transpiler avm-transpiler
-    SAVE ARTIFACT scripts/compile_then_transpile.sh
 
 format:
   FROM +source
diff --git a/avm-transpiler/scripts/compile_then_transpile.sh b/avm-transpiler/scripts/compile_then_transpile.sh
deleted file mode 100755
index 97303dd314b..00000000000
--- a/avm-transpiler/scripts/compile_then_transpile.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env bash
-# This is a wrapper script for nargo.
-# Pass any args that you'd normally pass to nargo.
-# If the first arg is "compile",
-# run nargo and then transpile any created artifacts.
-#
-# Usage: compile_then_transpile.sh [nargo args]
-set -eu
-
-NARGO=${NARGO:-nargo}
-TRANSPILER=${TRANSPILER:-avm-transpiler}
-
-if [ "${1:-}" != "compile" ]; then
-  # if not compiling, just pass through to nargo verbatim
-  $NARGO $@
-  exit $?
-fi
-shift # remove the compile arg so we can inject --show-artifact-paths
-
-# Forward all arguments to nargo, tee output to console
-artifacts_to_transpile=$($NARGO compile --show-artifact-paths $@ | tee /dev/tty | grep -oP 'Saved contract artifact to: \K.*')
-
-# NOTE: the output that is teed to /dev/tty will normally not be redirectable by the caller.
-# If the script is run via docker, however, the user will see this output on stdout and will be able to redirect.
-
-# Transpile each artifact
-# `$artifacts_to_transpile` needs to be unquoted here, otherwise it will break if there are multiple artifacts
-for artifact in $artifacts_to_transpile; do
-  # transpiler input and output files are the same (modify in-place)
-  $TRANSPILER "$artifact" "$artifact"
-done
diff --git a/aztec-nargo/Dockerfile b/aztec-nargo/Dockerfile
index 95473b82f59..2b9a48681da 100644
--- a/aztec-nargo/Dockerfile
+++ b/aztec-nargo/Dockerfile
@@ -4,16 +4,21 @@ FROM aztecprotocol/noir AS built-noir
 # to get built avm-transpiler binary
 FROM aztecprotocol/avm-transpiler AS built-transpiler
 
+# to get built barretenberg binary
+FROM --platform=linux/amd64 aztecprotocol/barretenberg-x86_64-linux-clang as barretenberg
+
 
 FROM ubuntu:noble
 # Install Tini as nargo doesn't handle signals properly.
 # Install git as nargo needs it to clone.
-RUN apt-get update && apt-get install -y git tini && rm -rf /var/lib/apt/lists/* && apt-get clean
+RUN apt-get update && apt-get install -y git tini jq && rm -rf /var/lib/apt/lists/* && apt-get clean
 
 # Copy binaries to /usr/bin
 COPY --from=built-noir /usr/src/noir/noir-repo/target/release/nargo /usr/bin/nargo
 COPY --from=built-transpiler /usr/src/avm-transpiler/target/release/avm-transpiler /usr/bin/avm-transpiler
+COPY --from=barretenberg /usr/src/barretenberg/cpp/build/bin/bb /usr/bin/bb
+
 # Copy in script that calls both binaries
-COPY ./avm-transpiler/scripts/compile_then_transpile.sh /usr/src/avm-transpiler/scripts/compile_then_transpile.sh
+COPY ./aztec-nargo/compile_then_postprocess.sh /usr/src/aztec-nargo/compile_then_postprocess.sh
 
-ENTRYPOINT ["/usr/bin/tini", "--", "/usr/src/avm-transpiler/scripts/compile_then_transpile.sh"]
+ENTRYPOINT ["/usr/bin/tini", "--", "/usr/src/aztec-nargo/compile_then_postprocess.sh"]
diff --git a/aztec-nargo/Earthfile b/aztec-nargo/Earthfile
index f0615e9bd65..49a32e4a3b4 100644
--- a/aztec-nargo/Earthfile
+++ b/aztec-nargo/Earthfile
@@ -4,17 +4,20 @@ run:
     FROM ubuntu:noble
     # Install Tini as nargo doesn't handle signals properly.
     # Install git as nargo needs it to clone.
-    RUN apt-get update && apt-get install -y git tini && rm -rf /var/lib/apt/lists/* && apt-get clean
+    RUN apt-get update && apt-get install -y git tini jq && rm -rf /var/lib/apt/lists/* && apt-get clean
 
     # Copy binaries to /usr/bin
     COPY ../noir+nargo/nargo /usr/bin/nargo
     COPY ../avm-transpiler+build/avm-transpiler /usr/bin/avm-transpiler
+    COPY ../barretenberg/cpp/+preset-release/bin/bb /usr/bin/bb
+
     # Copy in script that calls both binaries
-    COPY ../avm-transpiler+build/compile_then_transpile.sh /usr/bin/compile_then_transpile.sh
+    COPY ./compile_then_postprocess.sh /usr/bin/compile_then_postprocess.sh
 
     ENV PATH "/usr/bin:${PATH}"
-    ENTRYPOINT ["/usr/bin/tini", "--", "/usr/bin/compile_then_transpile.sh"]
+    ENTRYPOINT ["/usr/bin/tini", "--", "/usr/bin/compile_then_postprocess.sh"]
     SAVE IMAGE aztecprotocol/aztec-nargo
+    SAVE ARTIFACT /usr/bin/compile_then_postprocess.sh /aztec-nargo
 
 export-aztec-nargo:
   FROM +run
diff --git a/aztec-nargo/compile_then_postprocess.sh b/aztec-nargo/compile_then_postprocess.sh
new file mode 100755
index 00000000000..c859377609d
--- /dev/null
+++ b/aztec-nargo/compile_then_postprocess.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+# This is a wrapper script for nargo.
+# Pass any args that you'd normally pass to nargo.
+# If the first arg is "compile",
+# run nargo and then postprocess any created artifacts.
+#
+# Usage: compile_then_postprocess.sh [nargo args]
+set -eu
+
+NARGO=${NARGO:-nargo}
+TRANSPILER=${TRANSPILER:-avm-transpiler}
+BB=${BB:-bb}
+
+if [ "${1:-}" != "compile" ]; then
+  # if not compiling, just pass through to nargo verbatim
+  $NARGO $@
+  exit $?
+fi
+shift # remove the compile arg so we can inject --show-artifact-paths
+
+# Forward all arguments to nargo, tee output to console
+artifacts_to_process=$($NARGO compile --show-artifact-paths $@ | tee /dev/tty | grep -oP 'Saved contract artifact to: \K.*')
+
+# NOTE: the output that is teed to /dev/tty will normally not be redirectable by the caller.
+# If the script is run via docker, however, the user will see this output on stdout and will be able to redirect.
+
+# Postprocess each artifact
+# `$artifacts_to_process` needs to be unquoted here, otherwise it will break if there are multiple artifacts
+for artifact in $artifacts_to_process; do
+  # transpiler input and output files are the same (modify in-place)
+  $TRANSPILER "$artifact" "$artifact"
+  artifact_name=$(basename "$artifact")
+  echo "Generating verification keys for functions in $artifact_name"
+  # See contract_artifact.ts (getFunctionType) for reference
+  private_fn_indices=$(jq -r '.functions | to_entries | map(select((.value.custom_attributes | contains(["public"]) | not) and (.value.is_unconstrained == false))) | map(.key) | join(" ")' $artifact)
+  for fn_index in $private_fn_indices; do
+    fn_name=$(jq -r ".functions[$fn_index].name" $artifact)
+    fn_artifact=$(jq -r ".functions[$fn_index]" $artifact)
+    fn_artifact_path="$artifact.function_artifact_$fn_index.json"
+    echo $fn_artifact > $fn_artifact_path
+
+    echo "Generating verification key for function $fn_name"
+    # BB outputs the verification key to stdout as raw bytes, however, we need to base64 encode it before storing it in the artifact
+    verification_key=$($BB write_vk_mega_honk -h -b ${fn_artifact_path} -o - | base64)
+    rm $fn_artifact_path
+    jq ".functions[$fn_index].verification_key = \"$verification_key\"" $artifact > $artifact.tmp
+    mv $artifact.tmp $artifact
+  done
+done
diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.cpp
index 16553a32d4a..b9c1253eaf5 100644
--- a/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.cpp
+++ b/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.cpp
@@ -275,6 +275,15 @@ WASM_EXPORT void acir_vk_as_fields_ultra_honk(uint8_t const* vk_buf, fr::vec_out
 {
     using VerificationKey = UltraFlavor::VerificationKey;
 
+    auto verification_key = std::make_shared<VerificationKey>(from_buffer<VerificationKey>(vk_buf));
+    std::vector<bb::fr> vkey_as_fields = verification_key->to_field_elements();
+    *out_vkey = to_heap_buffer(vkey_as_fields);
+}
+
+WASM_EXPORT void acir_vk_as_fields_mega_honk(uint8_t const* vk_buf, fr::vec_out_buf out_vkey)
+{
+    using VerificationKey = MegaFlavor::VerificationKey;
+
     auto verification_key = std::make_shared<VerificationKey>(from_buffer<VerificationKey>(vk_buf));
     std::vector<bb::fr> vkey_as_fields = verification_key->to_field_elements();
     *out_vkey = to_heap_buffer(vkey_as_fields);
diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.hpp b/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.hpp
index 2ed4613b666..9efd50e4721 100644
--- a/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.hpp
+++ b/barretenberg/cpp/src/barretenberg/dsl/acir_proofs/c_bind.hpp
@@ -93,4 +93,6 @@ WASM_EXPORT void acir_write_vk_ultra_honk(uint8_t const* acir_vec, bool const* r
 
 WASM_EXPORT void acir_proof_as_fields_ultra_honk(uint8_t const* proof_buf, fr::vec_out_buf out);
 
-WASM_EXPORT void acir_vk_as_fields_ultra_honk(uint8_t const* vk_buf, fr::vec_out_buf out_vkey);
\ No newline at end of file
+WASM_EXPORT void acir_vk_as_fields_ultra_honk(uint8_t const* vk_buf, fr::vec_out_buf out_vkey);
+
+WASM_EXPORT void acir_vk_as_fields_mega_honk(uint8_t const* vk_buf, fr::vec_out_buf out_vkey);
\ No newline at end of file
diff --git a/barretenberg/exports.json b/barretenberg/exports.json
index 67c4854c607..83b2a64ec71 100644
--- a/barretenberg/exports.json
+++ b/barretenberg/exports.json
@@ -917,5 +917,21 @@
       }
     ],
     "isAsync": false
+  },
+  {
+    "functionName": "acir_vk_as_fields_mega_honk",
+    "inArgs": [
+      {
+        "name": "vk_buf",
+        "type": "const uint8_t *"
+      }
+    ],
+    "outArgs": [
+      {
+        "name": "out_vkey",
+        "type": "fr::vec_out_buf"
+      }
+    ],
+    "isAsync": false
   }
 ]
\ No newline at end of file
diff --git a/barretenberg/ts/src/barretenberg_api/index.ts b/barretenberg/ts/src/barretenberg_api/index.ts
index 06290eda204..a6ead4041b4 100644
--- a/barretenberg/ts/src/barretenberg_api/index.ts
+++ b/barretenberg/ts/src/barretenberg_api/index.ts
@@ -604,6 +604,18 @@ export class BarretenbergApi {
     const out = result.map((r, i) => outTypes[i].fromBuffer(r));
     return out[0];
   }
+
+  async acirVkAsFieldsMegaHonk(vkBuf: Uint8Array): Promise<Fr[]> {
+    const inArgs = [vkBuf].map(serializeBufferable);
+    const outTypes: OutputType[] = [VectorDeserializer(Fr)];
+    const result = await this.wasm.callWasmExport(
+      'acir_vk_as_fields_mega_honk',
+      inArgs,
+      outTypes.map(t => t.SIZE_IN_BYTES),
+    );
+    const out = result.map((r, i) => outTypes[i].fromBuffer(r));
+    return out[0];
+  }
 }
 export class BarretenbergApiSync {
   constructor(protected wasm: BarretenbergWasm) {}
@@ -1181,4 +1193,16 @@ export class BarretenbergApiSync {
     const out = result.map((r, i) => outTypes[i].fromBuffer(r));
     return out[0];
   }
+
+  acirVkAsFieldsMegaHonk(vkBuf: Uint8Array): Fr[] {
+    const inArgs = [vkBuf].map(serializeBufferable);
+    const outTypes: OutputType[] = [VectorDeserializer(Fr)];
+    const result = this.wasm.callWasmExport(
+      'acir_vk_as_fields_mega_honk',
+      inArgs,
+      outTypes.map(t => t.SIZE_IN_BYTES),
+    );
+    const out = result.map((r, i) => outTypes[i].fromBuffer(r));
+    return out[0];
+  }
 }
diff --git a/boxes/Dockerfile b/boxes/Dockerfile
index 6ca01d2f468..cd6a3ead2f9 100644
--- a/boxes/Dockerfile
+++ b/boxes/Dockerfile
@@ -2,17 +2,23 @@
 FROM aztecprotocol/aztec AS aztec
 FROM aztecprotocol/noir as noir
 FROM aztecprotocol/noir-projects as noir-projects
+FROM --platform=linux/amd64 aztecprotocol/barretenberg-x86_64-linux-clang as barretenberg
+FROM aztecprotocol/avm-transpiler AS transpiler
 
 # We need yarn. Start fresh container.
 FROM node:18.19.0
-RUN apt update && apt install netcat-openbsd
+RUN apt update && apt install netcat-openbsd jq
 COPY --from=aztec /usr/src /usr/src
-COPY --from=noir /usr/src/noir/noir-repo/target/release/nargo /usr/src/noir/noir-repo/target/release/nargo
 COPY --from=noir-projects /usr/src/noir-projects/aztec-nr /usr/src/noir-projects/aztec-nr
 COPY --from=noir-projects /usr/src/noir-projects/noir-protocol-circuits/crates/types /usr/src/noir-projects/noir-protocol-circuits/crates/types
+# Copy binaries to /usr/bin
+COPY --from=noir /usr/src/noir/noir-repo/target/release/nargo /usr/src/noir/noir-repo/target/release/nargo
+COPY --from=transpiler /usr/src/avm-transpiler/target/release/avm-transpiler /usr/bin/avm-transpiler
+COPY --from=barretenberg /usr/src/barretenberg/cpp/build/bin/bb /usr/bin/bb
+
 WORKDIR /usr/src/boxes
 COPY . .
-ENV AZTEC_NARGO=/usr/src/noir/noir-repo/target/release/nargo
+ENV AZTEC_NARGO=/usr/aztec-nargo/compile_then_postprocess.sh
 ENV AZTEC_BUILDER=/usr/src/yarn-project/builder/aztec-builder-dest
 RUN yarn
 RUN npx -y playwright@1.42 install --with-deps
diff --git a/boxes/Earthfile b/boxes/Earthfile
index 3c349f8aa49..d2febd89e91 100644
--- a/boxes/Earthfile
+++ b/boxes/Earthfile
@@ -31,10 +31,15 @@ build:
     COPY ../noir/+nargo/nargo /usr/src/noir/noir-repo/target/release/nargo
     COPY ../noir-projects/+build/aztec-nr /usr/src/noir-projects/aztec-nr
     COPY ../noir-projects/+build/noir-protocol-circuits/crates/types /usr/src/noir-projects/noir-protocol-circuits/crates/types
-
+    COPY ../avm-transpiler+build/avm-transpiler /usr/src/avm-transpiler/target/release/avm-transpiler
+    COPY ../barretenberg/cpp/+preset-release/bin/bb /usr/src/barretenberg/cpp/build/bin/bb
+    COPY ../aztec-nargo+run/aztec-nargo /usr/src/aztec-nargo
     WORKDIR /usr/src/boxes
 
-    ENV AZTEC_NARGO=/usr/src/noir/noir-repo/target/release/nargo
+    ENV NARGO=/usr/src/noir/noir-repo/target/release/nargo
+    ENV TRANSPILER=/usr/src/avm-transpiler/target/release/avm-transpiler
+    ENV BB=/usr/src/barretenberg/cpp/build/bin/bb
+    ENV AZTEC_NARGO=/usr/src/aztec-nargo
     ENV AZTEC_BUILDER=/usr/src/yarn-project/builder/aztec-builder-dest
     COPY . .
     RUN yarn build
diff --git a/build_manifest.yml b/build_manifest.yml
index e171cf14059..28982430bad 100644
--- a/build_manifest.yml
+++ b/build_manifest.yml
@@ -55,11 +55,8 @@ avm-transpiler:
 aztec-nargo:
   buildDir: .
   dockerfile: aztec-nargo/Dockerfile
-  rebuildPatterns:
-    - ^aztec-nargo/
-    - ^avm-transpiler/
-    - ^noir/
   dependencies:
+    - barretenberg-x86_64-linux-clang
     - avm-transpiler
     - noir
   multiarch: buildx
@@ -250,8 +247,10 @@ boxes:
   buildDir: boxes
   dependencies:
     - aztec
-    - noir
     - noir-projects
+    - noir
+    - avm-transpiler
+    - barretenberg-x86_64-linux-clang
   runDependencies:
     - aztec
 
diff --git a/noir-projects/noir-contracts/bootstrap.sh b/noir-projects/noir-contracts/bootstrap.sh
index b739b0eb429..655a7d638d0 100755
--- a/noir-projects/noir-contracts/bootstrap.sh
+++ b/noir-projects/noir-contracts/bootstrap.sh
@@ -19,20 +19,15 @@ echo "Compiling contracts..."
 NARGO=${NARGO:-../../noir/noir-repo/target/release/nargo}
 $NARGO compile --silence-warnings --inliner-aggressiveness 0
 
-echo "Generating protocol contract vks..."
+echo "Transpiling contracts..."
+scripts/transpile.sh
+
+echo "Postprocessing contracts..."
 BB_HASH=${BB_HASH:-$(cd ../../ && git ls-tree -r HEAD | grep 'barretenberg/cpp' | awk '{print $3}' | git hash-object --stdin)}
 echo Using BB hash $BB_HASH
-vkDir="./target/keys"
-mkdir -p $vkDir
+tempDir="./target/tmp"
+mkdir -p $tempDir
 
-protocol_contracts=$(jq -r '.[]' "./protocol_contracts.json")
-for contract in $protocol_contracts; do
-    artifactPath="./target/$contract.json"
-    fnNames=$(jq -r '.functions[] | select(.custom_attributes | index("private")) | .name' "$artifactPath")
-    for fnName in $fnNames; do
-      BB_HASH=$BB_HASH node ../scripts/generate_vk_json.js "$artifactPath" "$vkDir" "$fnName"
-    done
+for artifactPath in "./target"/*.json; do
+    BB_HASH=$BB_HASH node ./scripts/postprocess_contract.js "$artifactPath" "$tempDir"
 done
-
-echo "Transpiling contracts..."
-scripts/transpile.sh
\ No newline at end of file
diff --git a/noir-projects/noir-contracts/scripts/postprocess_contract.js b/noir-projects/noir-contracts/scripts/postprocess_contract.js
new file mode 100644
index 00000000000..b423bf2a50f
--- /dev/null
+++ b/noir-projects/noir-contracts/scripts/postprocess_contract.js
@@ -0,0 +1,104 @@
+const fs = require("fs/promises");
+const path = require("path");
+const {
+  BB_BIN_PATH,
+  readVKFromS3,
+  writeVKToS3,
+  generateArtifactHash,
+  getBarretenbergHash,
+} = require("../../scripts/verification_keys");
+const child_process = require("child_process");
+const crypto = require("crypto");
+
+function getFunctionArtifactPath(outputFolder, functionName) {
+  return path.join(outputFolder, `${functionName}.tmp.json`);
+}
+
+function getFunctionVkPath(outputFolder, functionName) {
+  return path.join(outputFolder, `${functionName}.vk.tmp.bin`);
+}
+
+async function getBytecodeHash({ bytecode }) {
+  if (!bytecode) {
+    throw new Error("No bytecode found in function artifact");
+  }
+  return crypto.createHash("md5").update(bytecode).digest("hex");
+}
+
+async function generateVkForFunction(functionArtifact, outputFolder) {
+  const functionArtifactPath = getFunctionArtifactPath(
+    outputFolder,
+    functionArtifact.name
+  );
+  const outputVkPath = getFunctionVkPath(outputFolder, functionArtifact.name);
+
+  await fs.writeFile(
+    functionArtifactPath,
+    JSON.stringify(functionArtifact, null, 2)
+  );
+
+  try {
+    const writeVkCommand = `${BB_BIN_PATH} write_vk_mega_honk -h -b "${functionArtifactPath}" -o "${outputVkPath}" `;
+
+    console.log("WRITE VK CMD: ", writeVkCommand);
+
+    await new Promise((resolve, reject) => {
+      child_process.exec(`${writeVkCommand}`, (err) => {
+        if (err) {
+          reject(err);
+        } else {
+          resolve();
+        }
+      });
+    });
+    const binaryVk = await fs.readFile(outputVkPath);
+    await fs.unlink(outputVkPath);
+
+    return binaryVk;
+  } finally {
+    await fs.unlink(functionArtifactPath);
+  }
+}
+
+async function main() {
+  let [artifactPath, tempFolder] = process.argv.slice(2);
+  const artifact = JSON.parse(await fs.readFile(artifactPath, "utf8"));
+  const barretenbergHash = await getBarretenbergHash();
+  for (const functionArtifact of artifact.functions.filter(
+    // See contract_artifact.ts (getFunctionType) for reference
+    (functionArtifact) =>
+      !functionArtifact.custom_attributes.includes("public") &&
+      !functionArtifact.is_unconstrained
+  )) {
+    const artifactName = `${artifact.name}-${functionArtifact.name}`;
+    const artifactHash = generateArtifactHash(
+      barretenbergHash,
+      await getBytecodeHash(functionArtifact),
+      true,
+      true
+    );
+    if (
+      functionArtifact.verification_key &&
+      functionArtifact.artifact_hash === artifactHash
+    ) {
+      console.log("Reusing on disk VK for", artifactName);
+    } else {
+      let vk = await readVKFromS3(artifactName, artifactHash, false);
+      if (!vk) {
+        vk = await generateVkForFunction(functionArtifact, tempFolder);
+        await writeVKToS3(artifactName, artifactHash, vk);
+      } else {
+        console.log("Using VK from remote cache for", artifactName);
+      }
+      functionArtifact.verification_key = vk.toString("base64");
+      functionArtifact.artifact_hash = artifactHash;
+    }
+  }
+
+  await fs.writeFile(artifactPath, JSON.stringify(artifact, null, 2));
+}
+
+main().catch((err) => {
+  console.error(err);
+  process.exit(1);
+});
diff --git a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/private_call_data_validator/validate_contract_address.nr b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/private_call_data_validator/validate_contract_address.nr
index b4b4e20086e..566cd989cbd 100644
--- a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/private_call_data_validator/validate_contract_address.nr
+++ b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/components/private_call_data_validator/validate_contract_address.nr
@@ -1,8 +1,8 @@
 use dep::types::{
     abis::private_kernel::private_call_data::PrivateCallData, address::AztecAddress,
-    constants::MAX_PROTOCOL_CONTRACTS, hash::stdlib_recursion_verification_key_compress_native_vk,
-    merkle_tree::root::root_from_sibling_path,
+    constants::MAX_PROTOCOL_CONTRACTS, merkle_tree::root::root_from_sibling_path,
 };
+use types::debug_log::debug_log_format;
 
 pub fn validate_contract_address(
     private_call_data: PrivateCallData,
@@ -11,13 +11,11 @@ pub fn validate_contract_address(
     let contract_address = private_call_data.public_inputs.call_context.contract_address;
     assert(!contract_address.is_zero(), "contract address cannot be zero");
 
-    // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3062): Why is this using a hash function from the stdlib::recursion namespace
-    let private_call_vk_hash =
-        stdlib_recursion_verification_key_compress_native_vk(private_call_data.vk);
+    private_call_data.vk.check_hash();
 
     let computed_address = AztecAddress::compute_from_private_function(
         private_call_data.public_inputs.call_context.function_selector,
-        private_call_vk_hash,
+        private_call_data.vk.hash,
         private_call_data.function_leaf_membership_witness,
         private_call_data.contract_class_artifact_hash,
         private_call_data.contract_class_public_bytecode_commitment,
@@ -26,6 +24,7 @@ pub fn validate_contract_address(
     );
 
     let protocol_contract_index = contract_address.to_field();
+
     let computed_protocol_contract_tree_root = if (MAX_PROTOCOL_CONTRACTS as Field).lt(
         protocol_contract_index,
     ) {
diff --git a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/tests/private_call_data_validator_builder/validate_contract_address.nr b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/tests/private_call_data_validator_builder/validate_contract_address.nr
index 7928b8e889b..dd7a5df1268 100644
--- a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/tests/private_call_data_validator_builder/validate_contract_address.nr
+++ b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/tests/private_call_data_validator_builder/validate_contract_address.nr
@@ -1,8 +1,7 @@
 use crate::tests::private_call_data_validator_builder::PrivateCallDataValidatorBuilder;
 use dep::types::address::AztecAddress;
-use std::embedded_curve_ops::{
-    EmbeddedCurvePoint, EmbeddedCurveScalar, fixed_base_scalar_mul as derive_public_key,
-};
+use std::embedded_curve_ops::{EmbeddedCurveScalar, fixed_base_scalar_mul as derive_public_key};
+use types::hash::verification_key_hash;
 
 impl PrivateCallDataValidatorBuilder {
     pub fn new_with_regular_contract() -> Self {
@@ -103,3 +102,25 @@ fn validate_contract_address_protocol_contract_wrong_computed_address_fails() {
 
     builder.validate();
 }
+
+#[test(should_fail_with = "Invalid VK hash")]
+fn validate_contract_address_wrong_vk_hash_fails() {
+    let mut builder = PrivateCallDataValidatorBuilder::new_with_regular_contract();
+
+    // Fake the vk hash so it doesn't match the preimage
+    builder.private_call.client_ivc_vk.hash = 27;
+
+    builder.validate();
+}
+
+#[test(should_fail_with = "computed contract address does not match expected one")]
+fn validate_contract_address_wrong_vk_fails() {
+    let mut builder = PrivateCallDataValidatorBuilder::new_with_regular_contract();
+
+    // Change the VK so the address doesn't match anymore
+    builder.private_call.client_ivc_vk.key[0] = 27;
+    builder.private_call.client_ivc_vk.hash =
+        verification_key_hash(builder.private_call.client_ivc_vk.key);
+
+    builder.validate();
+}
diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr b/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr
index 72f0c2485df..3dbaa0521a2 100644
--- a/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr
+++ b/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr
@@ -115,7 +115,7 @@ impl AztecAddress {
 
     pub fn compute_from_private_function(
         function_selector: FunctionSelector,
-        functino_vk_hash: Field,
+        function_vk_hash: Field,
         function_leaf_membership_witness: MembershipWitness<FUNCTION_TREE_HEIGHT>,
         contract_class_artifact_hash: Field,
         contract_class_public_bytecode_commitment: Field,
@@ -124,7 +124,7 @@ impl AztecAddress {
     ) -> Self {
         let private_functions_root = private_functions_root_from_siblings(
             function_selector,
-            functino_vk_hash,
+            function_vk_hash,
             function_leaf_membership_witness.leaf_index,
             function_leaf_membership_witness.sibling_path,
         );
diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr
index 5de0d2cf0f3..80ccd3129dd 100644
--- a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr
+++ b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr
@@ -129,16 +129,6 @@ pub fn merkle_hash(left: Field, right: Field) -> Field {
     poseidon2_hash([left, right])
 }
 
-pub fn stdlib_recursion_verification_key_compress_native_vk<let N: u32>(
-    _vk: VerificationKey<N>,
-) -> Field {
-    // Original cpp code
-    // stdlib::recursion::verification_key<CT::bn254>::compress_native(private_call.vk, GeneratorIndex::VK);
-    // The above cpp method is only ever called on verification key, so it has been special cased here
-    let _hash_index = GENERATOR_INDEX__VK;
-    0
-}
-
 pub fn compute_l2_to_l1_hash(
     contract_address: AztecAddress,
     recipient: EthAddress,
diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr
index f48dff6111c..9b90fff5998 100644
--- a/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr
+++ b/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr
@@ -41,10 +41,10 @@ use crate::{
     },
     address::{AztecAddress, EthAddress, SaltedInitializationHash},
     constants::{
-        FUNCTION_TREE_HEIGHT, MAX_ENCRYPTED_LOGS_PER_TX, MAX_FIELD_VALUE,
-        MAX_KEY_VALIDATION_REQUESTS_PER_TX, MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX,
-        MAX_L2_TO_L1_MSGS_PER_TX, MAX_NOTE_ENCRYPTED_LOGS_PER_TX,
-        MAX_NOTE_HASH_READ_REQUESTS_PER_TX, MAX_NOTE_HASHES_PER_TX,
+        CLIENT_IVC_VERIFICATION_KEY_LENGTH_IN_FIELDS, FUNCTION_TREE_HEIGHT,
+        MAX_ENCRYPTED_LOGS_PER_TX, MAX_FIELD_VALUE, MAX_KEY_VALIDATION_REQUESTS_PER_TX,
+        MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX, MAX_L2_TO_L1_MSGS_PER_TX,
+        MAX_NOTE_ENCRYPTED_LOGS_PER_TX, MAX_NOTE_HASH_READ_REQUESTS_PER_TX, MAX_NOTE_HASHES_PER_TX,
         MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX, MAX_NULLIFIER_READ_REQUESTS_PER_TX,
         MAX_NULLIFIERS_PER_TX, MAX_PRIVATE_CALL_STACK_LENGTH_PER_TX,
         MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, MAX_PUBLIC_DATA_READS_PER_CALL,
@@ -205,7 +205,10 @@ impl FixtureBuilder {
         let contract_data = fixtures::contracts::default_contract;
         let contract_function = fixtures::contract_functions::default_private_function;
 
-        builder.use_contract(contract_data).use_function(contract_function)
+        builder.use_contract(contract_data).use_function(
+            contract_function,
+            fixtures::contract_functions::default_vk,
+        )
     }
 
     pub fn as_parent_contract(&mut self) -> Self {
@@ -248,7 +251,7 @@ impl FixtureBuilder {
         let _ = self.use_contract(contract_data);
         self.contract_address = AztecAddress::from_field(contract_index as Field);
 
-        self.use_function(function_data)
+        self.use_function(function_data, fixtures::contract_functions::default_vk)
     }
 
     pub fn use_contract(&mut self, contract_data: ContractData) -> Self {
@@ -261,10 +264,15 @@ impl FixtureBuilder {
         *self
     }
 
-    pub fn use_function(&mut self, function_data: ContractFunction) -> Self {
+    pub fn use_function(
+        &mut self,
+        function_data: ContractFunction,
+        vk: [Field; CLIENT_IVC_VERIFICATION_KEY_LENGTH_IN_FIELDS],
+    ) -> Self {
         self.function_data = function_data.data;
         self.function_leaf_membership_witness = function_data.membership_witness;
         self.acir_hash = function_data.acir_hash;
+        self.client_ivc_vk = VerificationKey { key: vk, hash: function_data.vk_hash };
         *self
     }
 
diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixtures/contract_functions.nr b/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixtures/contract_functions.nr
index ffecfd4ef69..9a08c162fcc 100644
--- a/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixtures/contract_functions.nr
+++ b/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixtures/contract_functions.nr
@@ -1,5 +1,5 @@
 use crate::abis::{function_data::FunctionData, function_selector::FunctionSelector};
-use crate::constants::FUNCTION_TREE_HEIGHT;
+use crate::constants::{CLIENT_IVC_VERIFICATION_KEY_LENGTH_IN_FIELDS, FUNCTION_TREE_HEIGHT};
 use crate::merkle_tree::membership::MembershipWitness;
 
 pub struct ContractFunction {
@@ -9,10 +9,12 @@ pub struct ContractFunction {
     membership_witness: MembershipWitness<FUNCTION_TREE_HEIGHT>,
 }
 
+global default_vk = [0; CLIENT_IVC_VERIFICATION_KEY_LENGTH_IN_FIELDS];
+
 // sibling_path taken from __snapshots__/noir_test_gen.test.ts.snap
 global default_private_function = ContractFunction {
     data: FunctionData { selector: FunctionSelector { inner: 1010101 }, is_private: true },
-    vk_hash: 0,
+    vk_hash: crate::hash::verification_key_hash(default_vk),
     acir_hash: 1111,
     membership_witness: MembershipWitness {
         leaf_index: 0,
@@ -48,7 +50,7 @@ pub fn get_protocol_contract_function(contract_index: u32) -> ContractFunction {
             selector: FunctionSelector { inner: 98989 + contract_index },
             is_private: true,
         },
-        vk_hash: 0,
+        vk_hash: crate::hash::verification_key_hash(default_vk),
         acir_hash: 5555 + contract_index as Field,
         membership_witness: MembershipWitness {
             leaf_index: contract_index as Field,
diff --git a/noir-projects/scripts/generate_vk_json.js b/noir-projects/scripts/generate_vk_json.js
index 47aebef30d7..c891d1f7ca4 100644
--- a/noir-projects/scripts/generate_vk_json.js
+++ b/noir-projects/scripts/generate_vk_json.js
@@ -1,20 +1,16 @@
 const path = require("path");
 const fs = require("fs/promises");
-const fs_stream = require("fs");
 const child_process = require("child_process");
-
-const { fromIni } = require("@aws-sdk/credential-providers");
-const { S3 } = require("@aws-sdk/client-s3");
-
 const crypto = require("crypto");
 
 const megaHonkPatterns = require("../mega_honk_circuits.json");
-
-const BB_BIN_PATH =
-  process.env.BB_BIN ||
-  path.join(__dirname, "../../barretenberg/cpp/build/bin/bb");
-const BUCKET_NAME = "aztec-ci-artifacts";
-const PREFIX = "protocol";
+const {
+  readVKFromS3,
+  writeVKToS3,
+  getBarretenbergHash,
+  generateArtifactHash,
+  BB_BIN_PATH,
+} = require("./verification_keys");
 
 function vkBinaryFileNameForArtifactName(outputFolder, artifactName) {
   return path.join(outputFolder, `${artifactName}.vk`);
@@ -28,32 +24,6 @@ function vkDataFileNameForArtifactName(outputFolder, artifactName) {
   return path.join(outputFolder, `${artifactName}.vk.data.json`);
 }
 
-function getFunctionArtifactPath(outputFolder, functionName) {
-  return path.join(outputFolder, `${functionName}.tmp.json`);
-}
-
-async function createFunctionArtifact(
-  contractArtifactPath,
-  functionName,
-  outputFolder
-) {
-  const contractArtifact = JSON.parse(await fs.readFile(contractArtifactPath));
-  const artifact = contractArtifact.functions.find(
-    (fn) => fn.name === functionName
-  );
-  if (!artifact) {
-    throw new Error(`Cannot find artifact for function: ${functionName}.`);
-  }
-
-  const artifactPath = getFunctionArtifactPath(outputFolder, functionName);
-  await fs.writeFile(artifactPath, JSON.stringify(artifact, null, 2));
-  return artifactPath;
-}
-
-async function removeFunctionArtifact(artifactPath) {
-  await fs.unlink(artifactPath);
-}
-
 async function getBytecodeHash(artifactPath) {
   const { bytecode } = JSON.parse(await fs.readFile(artifactPath));
   if (!bytecode) {
@@ -62,37 +32,6 @@ async function getBytecodeHash(artifactPath) {
   return crypto.createHash("md5").update(bytecode).digest("hex");
 }
 
-function getBarretenbergHash() {
-  if (process.env.BB_HASH) {
-    return Promise.resolve(process.env.BB_HASH);
-  }
-  return new Promise((res, rej) => {
-    const hash = crypto.createHash("md5");
-
-    const rStream = fs_stream.createReadStream(BB_BIN_PATH);
-    rStream.on("data", (data) => {
-      hash.update(data);
-    });
-    rStream.on("end", () => {
-      res(hash.digest("hex"));
-    });
-    rStream.on("error", (err) => {
-      rej(err);
-    });
-  });
-}
-
-function generateArtifactHash(
-  barretenbergHash,
-  bytecodeHash,
-  isMegaHonk,
-  isRecursive
-) {
-  return `${barretenbergHash}-${bytecodeHash}-${
-    isMegaHonk ? "mega-honk" : "ultra-honk"
-  }-${isRecursive}`;
-}
-
 async function getArtifactHash(artifactPath, isMegaHonk, isRecursive) {
   const bytecodeHash = await getBytecodeHash(artifactPath);
   const barretenbergHash = await getBarretenbergHash();
@@ -128,12 +67,7 @@ function isMegaHonkCircuit(artifactName) {
   );
 }
 
-async function processArtifact(
-  artifactPath,
-  artifactName,
-  outputFolder,
-  syncWithS3
-) {
+async function processArtifact(artifactPath, artifactName, outputFolder) {
   const isMegaHonk = isMegaHonkCircuit(artifactName);
   const isRecursive = true;
 
@@ -151,9 +85,7 @@ async function processArtifact(
     return;
   }
 
-  let vkData = syncWithS3
-    ? await readVKFromS3(artifactName, artifactHash)
-    : undefined;
+  let vkData = await readVKFromS3(artifactName, artifactHash);
   if (!vkData) {
     vkData = await generateVKData(
       artifactName,
@@ -163,9 +95,7 @@ async function processArtifact(
       isMegaHonk,
       isRecursive
     );
-    if (syncWithS3) {
-      await writeVKToS3(artifactName, artifactHash, JSON.stringify(vkData));
-    }
+    await writeVKToS3(artifactName, artifactHash, JSON.stringify(vkData));
   } else {
     console.log("Using VK from remote cache for", artifactName);
   }
@@ -230,84 +160,19 @@ async function generateVKData(
 }
 
 async function main() {
-  let [artifactPath, outputFolder, functionName] = process.argv.slice(2);
+  let [artifactPath, outputFolder] = process.argv.slice(2);
   if (!artifactPath || !outputFolder) {
     console.log(
-      "Usage: node generate_vk_json.js <artifactPath> <outputFolder> [functionName]"
+      "Usage: node generate_vk_json.js <artifactPath> <outputFolder>"
     );
     return;
   }
 
-  const sourceArtifactPath = !functionName
-    ? artifactPath
-    : await createFunctionArtifact(artifactPath, functionName, outputFolder);
-
-  const artifactName = [
-    path.basename(artifactPath, ".json"),
-    functionName ? `-${functionName}` : "",
-  ].join("");
-
-  const syncWithS3 = true;
-
   await processArtifact(
-    sourceArtifactPath,
-    artifactName,
-    outputFolder,
-    syncWithS3
+    artifactPath,
+    path.basename(artifactPath, ".json"),
+    outputFolder
   );
-
-  if (sourceArtifactPath !== artifactPath) {
-    await removeFunctionArtifact(sourceArtifactPath);
-  }
-}
-
-function generateS3Client() {
-  return new S3({
-    credentials: fromIni({
-      profile: "default",
-    }),
-    region: "us-east-2",
-  });
-}
-
-async function writeVKToS3(artifactName, artifactHash, body) {
-  if (process.env.DISABLE_VK_S3_CACHE) {
-    return;
-  }
-  try {
-    const s3 = generateS3Client();
-    await s3.putObject({
-      Bucket: BUCKET_NAME,
-      Key: `${PREFIX}/${artifactName}-${artifactHash}.json`,
-      Body: body,
-    });
-  } catch (err) {
-    console.warn("Could not write to S3 VK remote cache", err.message);
-  }
-}
-
-async function readVKFromS3(artifactName, artifactHash) {
-  if (process.env.DISABLE_VK_S3_CACHE) {
-    return;
-  }
-  const key = `${PREFIX}/${artifactName}-${artifactHash}.json`;
-
-  try {
-    const s3 = generateS3Client();
-    const { Body: response } = await s3.getObject({
-      Bucket: BUCKET_NAME,
-      Key: key,
-    });
-
-    const result = JSON.parse(await response.transformToString());
-    return result;
-  } catch (err) {
-    console.warn(
-      `Could not read VK from remote cache at s3://${BUCKET_NAME}/${key}`,
-      err.message
-    );
-    return undefined;
-  }
 }
 
 main().catch((err) => {
diff --git a/noir-projects/scripts/verification_keys.js b/noir-projects/scripts/verification_keys.js
new file mode 100644
index 00000000000..90fd90cb991
--- /dev/null
+++ b/noir-projects/scripts/verification_keys.js
@@ -0,0 +1,104 @@
+const { fromIni } = require("@aws-sdk/credential-providers");
+const { S3 } = require("@aws-sdk/client-s3");
+const fs_stream = require("fs");
+const path = require("path");
+
+const BB_BIN_PATH =
+  process.env.BB_BIN ||
+  path.join(__dirname, "../../barretenberg/cpp/build/bin/bb");
+const BUCKET_NAME = "aztec-ci-artifacts";
+const PREFIX = "protocol";
+
+async function writeVKToS3(artifactName, artifactHash, body) {
+  if (process.env.DISABLE_VK_S3_CACHE) {
+    return;
+  }
+  try {
+    const s3 = generateS3Client();
+    await s3.putObject({
+      Bucket: BUCKET_NAME,
+      Key: `${PREFIX}/${artifactName}-${artifactHash}.json`,
+      Body: body,
+    });
+  } catch (err) {
+    console.warn("Could not write to S3 VK remote cache", err.message);
+  }
+}
+
+async function readVKFromS3(artifactName, artifactHash, json = true) {
+  if (process.env.DISABLE_VK_S3_CACHE) {
+    return;
+  }
+  const key = `${PREFIX}/${artifactName}-${artifactHash}.json`;
+
+  try {
+    const s3 = generateS3Client();
+    const { Body: response } = await s3.getObject({
+      Bucket: BUCKET_NAME,
+      Key: key,
+    });
+
+    if (json) {
+      const result = JSON.parse(await response.transformToString());
+      return result;
+    } else {
+      return Buffer.from(await response.transformToByteArray());
+    }
+  } catch (err) {
+    if (err.name !== "NoSuchKey") {
+      console.warn(
+        `Could not read VK from remote cache at s3://${BUCKET_NAME}/${key}`,
+        err.message
+      );
+    }
+    return undefined;
+  }
+}
+
+function generateS3Client() {
+  return new S3({
+    credentials: fromIni({
+      profile: "default",
+    }),
+    region: "us-east-2",
+  });
+}
+
+function generateArtifactHash(
+  barretenbergHash,
+  bytecodeHash,
+  isMegaHonk,
+  isRecursive
+) {
+  return `${barretenbergHash}-${bytecodeHash}-${
+    isMegaHonk ? "mega-honk" : "ultra-honk"
+  }-${isRecursive}`;
+}
+
+function getBarretenbergHash() {
+  if (process.env.BB_HASH) {
+    return Promise.resolve(process.env.BB_HASH);
+  }
+  return new Promise((res, rej) => {
+    const hash = crypto.createHash("md5");
+
+    const rStream = fs_stream.createReadStream(BB_BIN_PATH);
+    rStream.on("data", (data) => {
+      hash.update(data);
+    });
+    rStream.on("end", () => {
+      res(hash.digest("hex"));
+    });
+    rStream.on("error", (err) => {
+      rej(err);
+    });
+  });
+}
+
+module.exports = {
+  BB_BIN_PATH,
+  writeVKToS3,
+  readVKFromS3,
+  generateArtifactHash,
+  getBarretenbergHash,
+};
diff --git a/yarn-project/aztec.js/src/deployment/broadcast_function.ts b/yarn-project/aztec.js/src/deployment/broadcast_function.ts
index 6dcf5b6ec0a..599d2d4b63c 100644
--- a/yarn-project/aztec.js/src/deployment/broadcast_function.ts
+++ b/yarn-project/aztec.js/src/deployment/broadcast_function.ts
@@ -44,7 +44,7 @@ export async function broadcastPrivateFunction(
     privateFunctionTreeLeafIndex,
   } = createPrivateFunctionMembershipProof(selector, artifact);
 
-  const vkHash = computeVerificationKeyHash(privateFunctionArtifact.verificationKey!);
+  const vkHash = computeVerificationKeyHash(privateFunctionArtifact);
   const bytecode = bufferAsFields(
     privateFunctionArtifact.bytecode,
     MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS,
diff --git a/yarn-project/bb-prover/src/verification_key/verification_key_data.ts b/yarn-project/bb-prover/src/verification_key/verification_key_data.ts
index fe4c55a31cf..14f5eb28c59 100644
--- a/yarn-project/bb-prover/src/verification_key/verification_key_data.ts
+++ b/yarn-project/bb-prover/src/verification_key/verification_key_data.ts
@@ -4,7 +4,7 @@ import {
   VerificationKeyAsFields,
   VerificationKeyData,
 } from '@aztec/circuits.js';
-import { hashVk } from '@aztec/noir-protocol-circuits-types';
+import { hashVK } from '@aztec/circuits.js/hash';
 
 import { strict as assert } from 'assert';
 import * as fs from 'fs/promises';
@@ -25,7 +25,7 @@ export async function extractVkData(vkDirectoryPath: string): Promise<Verificati
   const fieldsJson = JSON.parse(rawFields);
   const fields = fieldsJson.map(Fr.fromString);
   // The hash is not included in the BB response
-  const vkHash = hashVk(fields);
+  const vkHash = hashVK(fields);
   const vkAsFields = new VerificationKeyAsFields(fields, vkHash);
   return new VerificationKeyData(vkAsFields, rawBinary);
 }
diff --git a/yarn-project/circuits.js/src/contract/contract_class.ts b/yarn-project/circuits.js/src/contract/contract_class.ts
index 28a8a019b45..8809a2bbe02 100644
--- a/yarn-project/circuits.js/src/contract/contract_class.ts
+++ b/yarn-project/circuits.js/src/contract/contract_class.ts
@@ -1,7 +1,9 @@
 import { type ContractArtifact, type FunctionArtifact, FunctionSelector, FunctionType } from '@aztec/foundation/abi';
+import { vkAsFieldsMegaHonk } from '@aztec/foundation/crypto';
 import { Fr } from '@aztec/foundation/fields';
 
 import { PUBLIC_DISPATCH_SELECTOR } from '../constants.gen.js';
+import { hashVK } from '../hash/hash.js';
 import { computeArtifactHash } from './artifact_hash.js';
 import { type ContractClassIdPreimage, computeContractClassIdWithPreimage } from './contract_class_id.js';
 import { type ContractClass, type ContractClassWithId, type PublicFunction } from './interfaces/index.js';
@@ -60,15 +62,16 @@ export function getContractClassPrivateFunctionFromArtifact(
 ): ContractClass['privateFunctions'][number] {
   return {
     selector: FunctionSelector.fromNameAndParameters(f.name, f.parameters),
-    vkHash: computeVerificationKeyHash(f.verificationKey!),
+    vkHash: computeVerificationKeyHash(f),
   };
 }
 
 /**
- * Calculates the hash of a verification key.
- * Returns zero for consistency with Noir.
+ * For a given private function, computes the hash of its vk.
  */
-export function computeVerificationKeyHash(_verificationKeyInBase64: string) {
-  // return Fr.fromBuffer(hashVK(Buffer.from(verificationKeyInBase64, 'hex')));
-  return Fr.ZERO;
+export function computeVerificationKeyHash(f: FunctionArtifact) {
+  if (!f.verificationKey) {
+    throw new Error(`Private function ${f.name} must have a verification key`);
+  }
+  return hashVK(vkAsFieldsMegaHonk(Buffer.from(f.verificationKey, 'base64')));
 }
diff --git a/yarn-project/circuits.js/src/contract/private_function_membership_proof.test.ts b/yarn-project/circuits.js/src/contract/private_function_membership_proof.test.ts
index c63a517957d..2135967d9d4 100644
--- a/yarn-project/circuits.js/src/contract/private_function_membership_proof.test.ts
+++ b/yarn-project/circuits.js/src/contract/private_function_membership_proof.test.ts
@@ -21,7 +21,7 @@ describe('private_function_membership_proof', () => {
     artifact = getBenchmarkContractArtifact();
     contractClass = getContractClassFromArtifact(artifact);
     privateFunction = artifact.functions.findLast(fn => fn.functionType === FunctionType.PRIVATE)!;
-    vkHash = computeVerificationKeyHash(privateFunction.verificationKey!);
+    vkHash = computeVerificationKeyHash(privateFunction);
     selector = FunctionSelector.fromNameAndParameters(privateFunction);
   });
 
diff --git a/yarn-project/circuits.js/src/hash/hash.ts b/yarn-project/circuits.js/src/hash/hash.ts
index 932fbcf54a9..a6ca04d5227 100644
--- a/yarn-project/circuits.js/src/hash/hash.ts
+++ b/yarn-project/circuits.js/src/hash/hash.ts
@@ -1,31 +1,17 @@
 import { type AztecAddress } from '@aztec/foundation/aztec-address';
-import { pedersenHashBuffer, poseidon2HashWithSeparator, sha256Trunc } from '@aztec/foundation/crypto';
+import { poseidon2Hash, poseidon2HashWithSeparator, sha256Trunc } from '@aztec/foundation/crypto';
 import { Fr } from '@aztec/foundation/fields';
-import { numToUInt8, numToUInt16BE, numToUInt32BE } from '@aztec/foundation/serialize';
 
 import { GeneratorIndex } from '../constants.gen.js';
 import { type ScopedL2ToL1Message } from '../structs/l2_to_l1_message.js';
-import { VerificationKey } from '../structs/verification_key.js';
 
 /**
  * Computes a hash of a given verification key.
- * @param vkBuf - The verification key.
+ * @param vkBuf - The verification key as fields.
  * @returns The hash of the verification key.
  */
-export function hashVK(vkBuf: Buffer) {
-  const vk = VerificationKey.fromBuffer(vkBuf);
-  const toHash = Buffer.concat([
-    numToUInt8(vk.circuitType),
-    numToUInt16BE(5), // fr::coset_generator(0)?
-    numToUInt32BE(vk.circuitSize),
-    numToUInt32BE(vk.numPublicInputs),
-    ...Object.values(vk.commitments)
-      .map(e => [e.y.toBuffer(), e.x.toBuffer()])
-      .flat(),
-    // Montgomery form of fr::one()? Not sure. But if so, why?
-    Buffer.from('1418144d5b080fcac24cdb7649bdadf246a6cb2426e324bedb94fb05118f023a', 'hex'),
-  ]);
-  return pedersenHashBuffer(toHash);
+export function hashVK(keyAsFields: Fr[]): Fr {
+  return poseidon2Hash(keyAsFields);
 }
 
 /**
diff --git a/yarn-project/foundation/src/abi/abi.ts b/yarn-project/foundation/src/abi/abi.ts
index a1d9c6aaab8..a762d4204f2 100644
--- a/yarn-project/foundation/src/abi/abi.ts
+++ b/yarn-project/foundation/src/abi/abi.ts
@@ -222,7 +222,7 @@ export interface FunctionAbi {
 export interface FunctionArtifact extends FunctionAbi {
   /** The ACIR bytecode of the function. */
   bytecode: Buffer;
-  /** The verification key of the function. */
+  /** The verification key of the function, base64 encoded, if it's a private fn. */
   verificationKey?: string;
   /** Maps opcodes to source code pointers */
   debugSymbols: string;
diff --git a/yarn-project/foundation/src/crypto/index.ts b/yarn-project/foundation/src/crypto/index.ts
index 4a93708f498..9385f359563 100644
--- a/yarn-project/foundation/src/crypto/index.ts
+++ b/yarn-project/foundation/src/crypto/index.ts
@@ -7,6 +7,7 @@ export * from './sha512/index.js';
 export * from './pedersen/index.js';
 export * from './poseidon/index.js';
 export * from './secp256k1-signer/index.js';
+export * from './keys/index.js';
 
 /**
  * Init the bb singleton. This constructs (if not already) the barretenberg sync api within bb.js itself.
diff --git a/yarn-project/foundation/src/crypto/keys/index.ts b/yarn-project/foundation/src/crypto/keys/index.ts
new file mode 100644
index 00000000000..7b2066717a2
--- /dev/null
+++ b/yarn-project/foundation/src/crypto/keys/index.ts
@@ -0,0 +1,9 @@
+import { BarretenbergSync, RawBuffer } from '@aztec/bb.js';
+
+import { Fr } from '../../fields/fields.js';
+
+export function vkAsFieldsMegaHonk(input: Buffer): Fr[] {
+  return BarretenbergSync.getSingleton()
+    .acirVkAsFieldsMegaHonk(new RawBuffer(input))
+    .map(bbFr => Fr.fromBuffer(Buffer.from(bbFr.toBuffer()))); // TODO(#4189): remove this conversion
+}
diff --git a/yarn-project/noir-protocol-circuits-types/src/index.ts b/yarn-project/noir-protocol-circuits-types/src/index.ts
index d592b373f22..bfda8a89930 100644
--- a/yarn-project/noir-protocol-circuits-types/src/index.ts
+++ b/yarn-project/noir-protocol-circuits-types/src/index.ts
@@ -108,7 +108,6 @@ export * from './artifacts.js';
 export { maxPrivateKernelResetDimensions, privateKernelResetDimensionsConfig } from './private_kernel_reset_data.js';
 export * from './utils/private_kernel_reset.js';
 export * from './vks.js';
-export { hashVk } from './utils/vk_json.js';
 
 /* eslint-disable camelcase */
 
diff --git a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_vk_hashes.ts b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_vk_hashes.ts
index d8bc6e0ac96..a0a2573e9e0 100644
--- a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_vk_hashes.ts
+++ b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_vk_hashes.ts
@@ -1,12 +1,11 @@
 import { Fr, VerificationKeyData } from '@aztec/circuits.js';
+import { hashVK } from '@aztec/circuits.js/hash';
 import { createConsoleLogger } from '@aztec/foundation/log';
 import { fileURLToPath } from '@aztec/foundation/url';
 
 import fs from 'fs/promises';
 import { join } from 'path';
 
-import { hashVk } from '../utils/vk_json.js';
-
 const log = createConsoleLogger('aztec:autogenerate');
 
 function resolveRelativePath(relativePath: string) {
@@ -34,7 +33,7 @@ const main = async () => {
       if (!content.vkHash) {
         const { keyAsFields } = content;
 
-        content.vkHash = hashVk(keyAsFields.map((str: string) => Fr.fromString(str))).toString();
+        content.vkHash = hashVK(keyAsFields.map((str: string) => Fr.fromString(str))).toString();
         await fs.writeFile(keyPath, JSON.stringify(content, null, 2));
       }
     }
diff --git a/yarn-project/noir-protocol-circuits-types/src/utils/vk_json.ts b/yarn-project/noir-protocol-circuits-types/src/utils/vk_json.ts
index 6176bcbb68f..2640a9e5cbb 100644
--- a/yarn-project/noir-protocol-circuits-types/src/utils/vk_json.ts
+++ b/yarn-project/noir-protocol-circuits-types/src/utils/vk_json.ts
@@ -1,5 +1,4 @@
 import { Fr, VerificationKeyAsFields, VerificationKeyData } from '@aztec/circuits.js';
-import { poseidon2Hash } from '@aztec/foundation/crypto';
 
 interface VkJson {
   keyAsBytes: string;
@@ -17,7 +16,3 @@ export function keyJsonToVKData(json: VkJson): VerificationKeyData {
     Buffer.from(keyAsBytes, 'hex'),
   );
 }
-
-export function hashVk(keyAsFields: Fr[]): Fr {
-  return poseidon2Hash(keyAsFields);
-}
diff --git a/yarn-project/protocol-contracts/src/protocol_contract_data.ts b/yarn-project/protocol-contracts/src/protocol_contract_data.ts
index db770ce0da4..3e901637877 100644
--- a/yarn-project/protocol-contracts/src/protocol_contract_data.ts
+++ b/yarn-project/protocol-contracts/src/protocol_contract_data.ts
@@ -50,14 +50,14 @@ export const ProtocolContractAddress: Record<ProtocolContractName, AztecAddress>
 };
 
 export const ProtocolContractLeaf = {
-  AuthRegistry: Fr.fromString('0x04d70cb3d8222ae04cfa59e8bfed4f804832aaaef4f485d1debb004d1b9d6362'),
-  ContractInstanceDeployer: Fr.fromString('0x04a661c9d4d295fc485a7e0f3de40c09b35366343bce8ad229106a8ef4076fe5'),
-  ContractClassRegisterer: Fr.fromString('0x147ba3294403576dbad10f86d3ffd4eb83fb230ffbcd5c8b153dd02942d0611f'),
-  MultiCallEntrypoint: Fr.fromString('0x154b701b41d6cf6da7204fef36b2ee9578b449d21b3792a9287bf45eba48fd26'),
-  FeeJuice: Fr.fromString('0x1067e9dc15d3046b6d21aaa8eafcfec88216217242cee3f9d722165ffc03c767'),
-  Router: Fr.fromString('0x16ab75e4efc0964c0ee3d715ac645d7972b722bfe60eea730a60b527c0681973'),
+  AuthRegistry: Fr.fromString('0x00d6c808f3c8a78645cce0ba37e17837da720d37a42d30814ce3aa80bb273e53'),
+  ContractInstanceDeployer: Fr.fromString('0x144e518ae79c22843ce5736fa723cbc072b93cb4508500f779037d5114c88310'),
+  ContractClassRegisterer: Fr.fromString('0x0503a6a49c9671be4b6d03be3db2bb36440631062755c776e9838e05a9afb1bd'),
+  MultiCallEntrypoint: Fr.fromString('0x2be4d47f4c42bf7c74e75387229c8e0cc89d0d086449122a5265abdf5ea70129'),
+  FeeJuice: Fr.fromString('0x1a47084d9b143a50a18292b2677588b3d575a473a0edc11466696f5e1f434fb1'),
+  Router: Fr.fromString('0x26d7b664d410b94a7b0543defc361669cae93382087d92f875a411910c695167'),
 };
 
 export const protocolContractTreeRoot = Fr.fromString(
-  '0x2673f1d0618d2c98ccb3a11282073002f73335c4791eac16f67bf522e24151d1',
+  '0x141e7aceb024c6b5aa82f9d5a9da7207bdb2953679674a5ee306290a193d674c',
 );
diff --git a/yarn-project/protocol-contracts/src/scripts/generate_data.ts b/yarn-project/protocol-contracts/src/scripts/generate_data.ts
index d42345c68e9..0d226bcc99f 100644
--- a/yarn-project/protocol-contracts/src/scripts/generate_data.ts
+++ b/yarn-project/protocol-contracts/src/scripts/generate_data.ts
@@ -50,10 +50,6 @@ async function clearDestDir() {
   await fs.mkdir(destArtifactsDir, { recursive: true });
 }
 
-function getPrivateFunctionNames(artifact: NoirCompiledContract) {
-  return artifact.functions.filter(fn => fn.custom_attributes.includes('private')).map(fn => fn.name);
-}
-
 async function copyArtifact(srcName: string, destName: string) {
   const src = path.join(srcPath, `${srcName}.json`);
   const artifact = JSON.parse(await fs.readFile(src, 'utf8')) as NoirCompiledContract;
@@ -62,17 +58,6 @@ async function copyArtifact(srcName: string, destName: string) {
   return artifact;
 }
 
-async function copyVks(srcName: string, destName: string, fnNames: string[]) {
-  const deskVksDir = path.join(destArtifactsDir, 'keys', destName);
-  await fs.mkdir(deskVksDir, { recursive: true });
-
-  for (const fnName of fnNames) {
-    const src = path.join(srcPath, 'keys', `${srcName}-${fnName}.vk.data.json`);
-    const dest = path.join(deskVksDir, `${fnName}.vk.data.json`);
-    await fs.copyFile(src, dest);
-  }
-}
-
 function computeContractLeaf(artifact: NoirCompiledContract) {
   const instance = getContractInstanceFromDeployParams(loadContractArtifact(artifact), { salt });
   return instance.address;
@@ -191,8 +176,6 @@ async function main() {
     const srcName = srcNames[i];
     const destName = destNames[i];
     const artifact = await copyArtifact(srcName, destName);
-    const fnNames = getPrivateFunctionNames(artifact);
-    await copyVks(srcName, destName, fnNames);
     await generateDeclarationFile(destName);
     leaves.push(computeContractLeaf(artifact));
   }
diff --git a/yarn-project/pxe/src/kernel_prover/kernel_prover.ts b/yarn-project/pxe/src/kernel_prover/kernel_prover.ts
index d4ce8006693..0a949026414 100644
--- a/yarn-project/pxe/src/kernel_prover/kernel_prover.ts
+++ b/yarn-project/pxe/src/kernel_prover/kernel_prover.ts
@@ -23,7 +23,9 @@ import {
   VK_TREE_HEIGHT,
   VerificationKeyAsFields,
 } from '@aztec/circuits.js';
+import { hashVK } from '@aztec/circuits.js/hash';
 import { makeTuple } from '@aztec/foundation/array';
+import { vkAsFieldsMegaHonk } from '@aztec/foundation/crypto';
 import { createDebugLogger } from '@aztec/foundation/log';
 import { assertLength } from '@aztec/foundation/serialize';
 import { pushTestData } from '@aztec/foundation/testing';
@@ -143,8 +145,7 @@ export class KernelProver {
         await addGateCount(functionName as string, currentExecution.acir);
       }
 
-      const appVk = await this.proofCreator.computeAppCircuitVerificationKey(currentExecution.acir, functionName);
-      const privateCallData = await this.createPrivateCallData(currentExecution, appVk.verificationKey);
+      const privateCallData = await this.createPrivateCallData(currentExecution);
 
       if (firstIteration) {
         const proofInput = new PrivateKernelInitCircuitPrivateInputs(
@@ -241,9 +242,12 @@ export class KernelProver {
     return tailOutput;
   }
 
-  private async createPrivateCallData({ publicInputs }: PrivateExecutionResult, vk: VerificationKeyAsFields) {
+  private async createPrivateCallData({ publicInputs, vk: vkAsBuffer }: PrivateExecutionResult) {
     const { contractAddress, functionSelector } = publicInputs.callContext;
 
+    const vkAsFields = vkAsFieldsMegaHonk(vkAsBuffer);
+    const vk = new VerificationKeyAsFields(vkAsFields, hashVK(vkAsFields));
+
     const functionLeafMembershipWitness = await this.oracle.getFunctionMembershipWitness(
       contractAddress,
       functionSelector,
diff --git a/yarn-project/simulator/src/client/private_execution.ts b/yarn-project/simulator/src/client/private_execution.ts
index 0d117501df2..2a4512be96c 100644
--- a/yarn-project/simulator/src/client/private_execution.ts
+++ b/yarn-project/simulator/src/client/private_execution.ts
@@ -76,7 +76,7 @@ export async function executePrivateFunction(
 
   return new PrivateExecutionResult(
     acir,
-    Buffer.from(artifact.verificationKey!, 'hex'),
+    Buffer.from(artifact.verificationKey!, 'base64'),
     partialWitness,
     publicInputs,
     noteHashLeafIndexMap,
diff --git a/yarn-project/types/src/abi/contract_artifact.ts b/yarn-project/types/src/abi/contract_artifact.ts
index 73896129f2f..e6c12bd4427 100644
--- a/yarn-project/types/src/abi/contract_artifact.ts
+++ b/yarn-project/types/src/abi/contract_artifact.ts
@@ -23,7 +23,6 @@ import {
   AZTEC_VIEW_ATTRIBUTE,
   type NoirCompiledContract,
 } from '../noir/index.js';
-import { mockVerificationKey } from './mocked_keys.js';
 
 /**
  * Serializes a contract artifact to a buffer for storage.
@@ -129,7 +128,7 @@ function generateFunctionParameter(param: NoirCompiledContractFunctionParameter)
 type NoirCompiledContractFunction = NoirCompiledContract['functions'][number];
 
 /**
- * Generates a function build artifact. Replaces verification key with a mock value.
+ * Generates a function build artifact.
  * @param fn - Noir function entry.
  * @param contract - Parent contract.
  * @returns Function artifact.
@@ -180,7 +179,7 @@ function generateFunctionArtifact(fn: NoirCompiledContractFunction, contract: No
     parameters,
     returnTypes,
     bytecode: Buffer.from(fn.bytecode, 'base64'),
-    verificationKey: mockVerificationKey,
+    verificationKey: fn.verification_key,
     debugSymbols: fn.debug_symbols,
     errorTypes: fn.abi.error_types,
   };