From a58e489fc45462d1005542bf336510a3a54fc7e3 Mon Sep 17 00:00:00 2001 From: dbanks12 Date: Sun, 17 May 2026 14:17:24 +0000 Subject: [PATCH] feat(standard-contracts): prototype C++-driven address derivation in bb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `bb derive_standard_contract_addresses` subcommand that reads freshly-built Noir artifacts and stamps the standard-contract addresses into `standard_addresses.nr` (in both the aztec-nr/aztec crate and the aztec_sublib twin) before the rest of noir-contracts compiles against them. This breaks the build-order chicken-and-egg in the current TS-based generator: the TS generator depends on the yarn-project build, but the addresses need to be baked in before noir-contracts finishes. For auth_registry and public_checks the derived address is byte-identical to the TS-derived one (verified against the existing pinned values). Known prototype limitations: - `artifact_hash` and `private_functions_root` are READ from a sidecar manifest (`standard_contract_class_id_preimages.json`) instead of being derived from the artifact in C++. Porting their TS derivation (sha256-reduced-to-Fr merkle over selectors + metadata hashes, plus deterministic-JSON-stringify of contract outputs) is the natural follow-up. - Bootstrap integration only handles auth_registry + public_checks; extending to multi_call_entrypoint is mechanical. - The TS generator (`generate:data`) is left in place as a drift check — it still produces the same `standard_addresses.nr`, so any divergence fails the build loudly. --- .../api/standard_address_derivation.cpp | 485 ++++++++++++++++++ .../api/standard_address_derivation.hpp | 48 ++ barretenberg/cpp/src/barretenberg/bb/cli.cpp | 32 ++ noir-projects/noir-contracts/bootstrap.sh | 107 +++- .../standard_contract_class_id_preimages.json | 15 + 5 files changed, 677 insertions(+), 10 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/api/standard_address_derivation.cpp create mode 100644 barretenberg/cpp/src/barretenberg/api/standard_address_derivation.hpp create mode 100644 yarn-project/standard-contracts/src/standard_contract_class_id_preimages.json diff --git a/barretenberg/cpp/src/barretenberg/api/standard_address_derivation.cpp b/barretenberg/cpp/src/barretenberg/api/standard_address_derivation.cpp new file mode 100644 index 000000000000..ba0ed21c83f4 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/api/standard_address_derivation.cpp @@ -0,0 +1,485 @@ +#ifndef __wasm__ +#include "barretenberg/api/standard_address_derivation.hpp" + +#include "barretenberg/api/file_io.hpp" +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/common/base64.hpp" +#include "barretenberg/common/log.hpp" +#include "barretenberg/common/throw_or_abort.hpp" +#include "barretenberg/crypto/poseidon2/poseidon2.hpp" +#include "barretenberg/crypto/poseidon2/poseidon2_params.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" +#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bb { + +namespace { + +using FF = ::bb::fr; +using poseidon2 = crypto::Poseidon2; + +// Domain separator not in aztec_constants.hpp at the time of writing. Mirrored from +// yarn-project/constants/src/constants.gen.ts (DomainSeparator enum). The follow-up that ports +// `computePrivateFunctionsRoot` to C++ will also need DOM_SEP__PRIVATE_FUNCTION_LEAF (1389398688) +// and DOM_SEP__FUNCTION_ARGS (3576554347) and FUNCTION_TREE_HEIGHT (7). +constexpr uint32_t DOM_SEP__INITIALIZER = 385396519UL; + +// Default public key components (PublicKeys.default() in TS). Decimal values from +// yarn-project/constants/src/constants.gen.ts. These are points on grumpkin; the address +// derivation uses incoming_viewing_key in scalar-multiply form. +const char* const DEFAULT_NPK_M_X = "582240093077765400562621227108555700500271598878376310175765873770292988861"; +const char* const DEFAULT_NPK_M_Y = "10422444662424639723529825114205836958711284159673861467999592572974769103684"; +const char* const DEFAULT_IVPK_M_X = "339708709767762472786445938838804872781183545349360029270386718856175781484"; +const char* const DEFAULT_IVPK_M_Y = "12719619215050539905199178334954929730355853796706924300730604757520758976849"; +const char* const DEFAULT_OVPK_M_X = "12212787719617305570587928860288475454328008955283046946846066128763901043335"; +const char* const DEFAULT_OVPK_M_Y = "3646747884782549389807830220601404629716007431341772952958971658285958854707"; +const char* const DEFAULT_TPK_M_X = "728059161893070741164607238299536939695876538801885465230641192969135857403"; +const char* const DEFAULT_TPK_M_Y = "14575718736702206050102425029229426215631664471161015518982549597389390371695"; + +// Decimal string -> Fr. `numeric::uint256_t` only parses fully-padded hex, so this routes +// through a manual base-10 accumulator. +FF parse_field_decimal(const char* dec) +{ + numeric::uint256_t acc = 0; + const numeric::uint256_t ten = 10; + for (const char* p = dec; *p != '\0'; ++p) { + if (*p < '0' || *p > '9') { + throw_or_abort(std::string("parse_field_decimal: invalid digit in ") + dec); + } + acc = acc * ten + numeric::uint256_t(static_cast(*p - '0')); + } + return FF(acc); +} + +// Hex string -> Fr. Accepts with-or-without 0x prefix and any length up to 64 hex digits; +// pads to the 64-digit form expected by `numeric::uint256_t`'s string constructor. +FF parse_field_hex(const std::string& hex) +{ + std::string s = hex; + if (s.rfind("0x", 0) == 0 || s.rfind("0X", 0) == 0) { + s = s.substr(2); + } + if (s.size() > 64) { + throw_or_abort("parse_field_hex: hex string too long"); + } + s = std::string(64 - s.size(), '0') + s; + return FF(numeric::uint256_t(s)); +} + +// Format a field as a `0x`-prefixed, zero-padded 64-hex-digit string. Matches the output of +// Fr.toString() in TS, which is what the existing standard_addresses.nr generator emits. +// `numeric::uint256_t::operator<<` already prepends "0x" and zero-pads each 16-hex-digit limb, +// so streaming the value directly produces the same shape as the TS output. +std::string field_to_padded_hex(const FF& f) +{ + numeric::uint256_t v(f); + std::ostringstream oss; + oss << v; + return oss.str(); +} + +// Mirror of `encode_bytecode` in vm2/simulation/lib/contract_crypto.cpp. Reimplemented here to +// avoid linking vm2 into bb (which uses vm2_stub). Encodes bytecode as field elements packing +// 31 bytes per field in big-endian order. +std::vector encode_bytecode_as_fields(const std::vector& bytecode) +{ + std::vector result; + size_t bytecode_len = bytecode.size(); + if (bytecode_len == 0) { + return result; + } + result.reserve((bytecode_len + 30) / 31); + + for (size_t i = 0; i < bytecode_len; i += 31) { + numeric::uint256_t as_int = 0; + if (bytecode_len - i >= 32) { + // Read 32 bytes big-endian directly. + for (size_t b = 0; b < 32; ++b) { + as_int = (as_int << 8) | numeric::uint256_t(bytecode[i + b]); + } + } else { + std::vector tail(32, 0); + for (size_t b = 0; b < bytecode_len - i; ++b) { + tail[b] = bytecode[i + b]; + } + for (size_t b = 0; b < 32; ++b) { + as_int = (as_int << 8) | numeric::uint256_t(tail[b]); + } + } + result.push_back(FF(as_int >> 8)); + } + return result; +} + +// Mirror of `compute_public_bytecode_commitment` in vm2/simulation/lib/contract_crypto.cpp. The +// domain separator prepends `DOM_SEP__PUBLIC_BYTECODE | (byte_len << 32)`. +FF compute_public_bytecode_commitment(const std::vector& bytecode) +{ + auto fields = encode_bytecode_as_fields(bytecode); + numeric::uint256_t sep_val = + numeric::uint256_t(DOM_SEP__PUBLIC_BYTECODE) + (numeric::uint256_t(bytecode.size()) << 32); + std::vector inputs; + inputs.reserve(1 + fields.size()); + inputs.push_back(FF(sep_val)); + inputs.insert(inputs.end(), fields.begin(), fields.end()); + return poseidon2::hash(inputs); +} + +// poseidon2(separator, ...inputs). +FF poseidon2_hash_with_separator(uint32_t separator, const std::vector& inputs) +{ + std::vector with_sep; + with_sep.reserve(inputs.size() + 1); + with_sep.push_back(FF(numeric::uint256_t(separator))); + with_sep.insert(with_sep.end(), inputs.begin(), inputs.end()); + return poseidon2::hash(with_sep); +} + +// Build the default `PublicKeys` (matches `PublicKeys.default()` in stdlib). +struct DefaultPublicKeys { + grumpkin::g1::affine_element nullifier_key; + grumpkin::g1::affine_element incoming_viewing_key; + grumpkin::g1::affine_element outgoing_viewing_key; + grumpkin::g1::affine_element tagging_key; + + static DefaultPublicKeys instance() + { + DefaultPublicKeys k; + k.nullifier_key = + grumpkin::g1::affine_element(parse_field_decimal(DEFAULT_NPK_M_X), parse_field_decimal(DEFAULT_NPK_M_Y)); + k.incoming_viewing_key = + grumpkin::g1::affine_element(parse_field_decimal(DEFAULT_IVPK_M_X), parse_field_decimal(DEFAULT_IVPK_M_Y)); + k.outgoing_viewing_key = + grumpkin::g1::affine_element(parse_field_decimal(DEFAULT_OVPK_M_X), parse_field_decimal(DEFAULT_OVPK_M_Y)); + k.tagging_key = + grumpkin::g1::affine_element(parse_field_decimal(DEFAULT_TPK_M_X), parse_field_decimal(DEFAULT_TPK_M_Y)); + return k; + } +}; + +// Mirror of `hash_public_keys` in vm2/simulation/lib/contract_crypto.cpp: poseidon2 over +// (DOM_SEP__PUBLIC_KEYS_HASH, then for each key: x, y, 0 [is_infinity placeholder]). +FF hash_public_keys(const DefaultPublicKeys& keys) +{ + std::vector inputs; + inputs.push_back(FF(numeric::uint256_t(DOM_SEP__PUBLIC_KEYS_HASH))); + auto add = [&](const grumpkin::g1::affine_element& p) { + inputs.push_back(p.x); + inputs.push_back(p.y); + inputs.push_back(FF::zero()); + }; + add(keys.nullifier_key); + add(keys.incoming_viewing_key); + add(keys.outgoing_viewing_key); + add(keys.tagging_key); + return poseidon2::hash(inputs); +} + +// Mirror of `compute_contract_address` in vm2/simulation/lib/contract_crypto.cpp. Performs the +// salted-init -> partial-address -> public-keys-hash -> (G*h + ivpk).x chain. +FF compute_contract_address(const FF& original_class_id, + const FF& initialization_hash, + const FF& salt, + const FF& deployer, + const DefaultPublicKeys& keys) +{ + FF salted_init = + poseidon2_hash_with_separator(DOM_SEP__SALTED_INITIALIZATION_HASH, { salt, initialization_hash, deployer }); + FF partial = poseidon2_hash_with_separator(DOM_SEP__PARTIAL_ADDRESS, { original_class_id, salted_init }); + FF public_keys_hash = hash_public_keys(keys); + FF h = poseidon2_hash_with_separator(DOM_SEP__CONTRACT_ADDRESS_V1, { public_keys_hash, partial }); + grumpkin::fr h_fq = grumpkin::fr(h); + auto result = (grumpkin::g1::affine_one * h_fq + keys.incoming_viewing_key); + return result.x; +} + +// Render a Noir signature string matching `decodeFunctionSignature` in TS. Used for +// FunctionSelector.fromNameAndParameters. +std::string render_abi_type(const nlohmann::json& type); + +std::string render_struct_fields(const nlohmann::json& fields) +{ + std::ostringstream oss; + oss << "("; + bool first = true; + for (const auto& field : fields) { + if (!first) { + oss << ","; + } + first = false; + oss << render_abi_type(field["type"]); + } + oss << ")"; + return oss.str(); +} + +std::string render_abi_type(const nlohmann::json& type) +{ + const std::string kind = type["kind"].get(); + if (kind == "field") { + return "Field"; + } + if (kind == "integer") { + const auto sign = type["sign"].get(); + const auto width = type["width"].get(); + return (sign == "signed" ? "i" : "u") + std::to_string(width); + } + if (kind == "boolean") { + return "bool"; + } + if (kind == "array") { + return "[" + render_abi_type(type["type"]) + ";" + std::to_string(type["length"].get()) + "]"; + } + if (kind == "string") { + return "str<" + std::to_string(type["length"].get()) + ">"; + } + if (kind == "struct") { + return render_struct_fields(type["fields"]); + } + throw_or_abort("Unsupported AbiType kind: " + kind); + return ""; +} + +std::string render_function_signature(const std::string& name, const nlohmann::json& parameters) +{ + std::ostringstream oss; + oss << name << "("; + bool first = true; + for (const auto& param : parameters) { + if (!first) { + oss << ","; + } + first = false; + oss << render_abi_type(param["type"]); + } + oss << ")"; + return oss.str(); +} + +// Mirror of `FunctionSelector.fromSignature` in TS: poseidon2-hash the signature bytes +// (31-byte chunks, little-endian per chunk per TS quirk) then take the last 4 big-endian bytes. +// We compute it as a Field (the selector is later used as a Field anyway in the merkle leaf). +FF function_selector_from_signature(const std::string& signature) +{ + // poseidon2HashBytes: split into 31-byte chunks, reverse each (little-endian per chunk), + // pack into a Field, then poseidon2-hash the Field array. + std::vector field_inputs; + for (size_t i = 0; i < signature.size(); i += 31) { + std::vector chunk(32, 0); + size_t copy_len = std::min(31, signature.size() - i); + for (size_t j = 0; j < copy_len; ++j) { + chunk[j] = static_cast(signature[i + j]); + } + // Reverse the 32-byte chunk (TS does fieldBytes.reverse()). + std::reverse(chunk.begin(), chunk.end()); + numeric::uint256_t as_int = 0; + for (size_t b = 0; b < 32; ++b) { + as_int = (as_int << 8) | numeric::uint256_t(chunk[b]); + } + field_inputs.push_back(FF(as_int)); + } + FF h = poseidon2::hash(field_inputs); + + // Take the last 4 big-endian bytes (Selector.SIZE == 4). + numeric::uint256_t h_int(h); + // Mask to last 32 bits. + constexpr uint32_t SELECTOR_MASK = 0xFFFFFFFFu; + uint32_t last4 = static_cast(static_cast(h_int.data[0]) & SELECTOR_MASK); + return FF(numeric::uint256_t(last4)); +} + +FF compute_function_selector(const nlohmann::json& function) +{ + const std::string& name = function["name"].get_ref(); + return function_selector_from_signature(render_function_signature(name, function["abi"]["parameters"])); +} + +// Compute initialization hash for an empty-args constructor: poseidon2(INITIALIZER, [selector, 0]) +// (varargs hash of [] == Fr.ZERO). +FF compute_init_hash_empty_args(const nlohmann::json& constructor_fn) +{ + if (constructor_fn.is_null()) { + return FF::zero(); + } + FF selector = compute_function_selector(constructor_fn); + FF args_hash = FF::zero(); // computeVarArgsHash([]) returns Fr.ZERO. + return poseidon2_hash_with_separator(DOM_SEP__INITIALIZER, { selector, args_hash }); +} + +bool function_has_attribute(const nlohmann::json& function, const std::string& attribute) +{ + if (!function.contains("custom_attributes") || !function["custom_attributes"].is_array()) { + return false; + } + for (const auto& attr : function["custom_attributes"]) { + if (attr.is_string() && attr.get_ref() == attribute) { + return true; + } + } + return false; +} + +// Find the `public_dispatch` function's bytecode. Mirrors TS's +// `getContractClassFromArtifact`, which uses the lone public function as the packed bytecode +// (public_dispatch is the only public function retained in the loaded artifact). +// +// IMPORTANT: TS's `computePublicBytecodeCommitment` treats `bytecode` as a base64-decoded buffer +// without gunzipping it. This differs from how `bb aztec_process` reads ACIR bytecode for VK +// derivation (which DOES gunzip). The TS chain hashes the gzipped bytes directly, so we mirror +// that here with `base64_decode` (no decompression). +std::vector get_public_dispatch_bytecode(const nlohmann::json& artifact) +{ + for (const auto& fn : artifact["functions"]) { + if (fn["name"].is_string() && fn["name"].get_ref() == "public_dispatch") { + const auto& base64_bytecode = fn["bytecode"].get(); + std::string decoded = base64_decode(base64_bytecode, /*remove_linebreaks=*/false); + return std::vector(decoded.begin(), decoded.end()); + } + } + return {}; +} + +const nlohmann::json* find_initializer_function(const nlohmann::json& artifact) +{ + for (const auto& fn : artifact["functions"]) { + if (function_has_attribute(fn, "abi_initializer")) { + return &fn; + } + } + return nullptr; +} + +struct DerivationEntry { + std::filesystem::path artifact_path; + std::string nr_const; + FF artifact_hash; + FF private_functions_root; + FF salt; + FF deployer; + FF expected_address; // Optional sanity-check; zero means "not provided". +}; + +// Render a single line of the generated Noir module. +std::string render_noir_global(const std::string& nr_const, const FF& address) +{ + std::ostringstream oss; + oss << "pub global " << nr_const << ": AztecAddress = AztecAddress::from_field(\n " + << field_to_padded_hex(address) << ",\n);"; + return oss.str(); +} + +// Mirrors `renderNoirAddresses` in generate_data.ts: same `nargo fmt`-stable layout. +std::string render_noir_module(const std::vector>& rows) +{ + std::ostringstream oss; + oss << "// GENERATED FILE - DO NOT EDIT. RUN `yarn workspace @aztec/standard-contracts run generate`.\n"; + oss << "use protocol_types::{address::AztecAddress, traits::FromField};\n"; + bool first = true; + for (const auto& row : rows) { + oss << (first ? "\n" : "\n\n"); + first = false; + oss << render_noir_global(row.first, row.second); + } + oss << "\n"; + return oss.str(); +} + +} // anonymous namespace + +bool derive_standard_contract_addresses(const std::filesystem::path& config_path) +{ + if (!std::filesystem::exists(config_path)) { + info("Config file not found: ", config_path.string()); + return false; + } + + auto config_content = read_file(config_path); + auto config = nlohmann::json::parse(std::string(config_content.begin(), config_content.end())); + + std::vector entries; + for (const auto& entry : config["entries"]) { + DerivationEntry e; + e.artifact_path = entry["artifact_path"].get(); + e.nr_const = entry["nr_const"].get(); + e.artifact_hash = parse_field_hex(entry["artifact_hash"].get()); + e.private_functions_root = parse_field_hex(entry["private_functions_root"].get()); + e.salt = parse_field_hex(entry.value("salt", std::string("0x1"))); + e.deployer = parse_field_hex(entry.value("deployer", std::string("0x0"))); + if (entry.contains("expected_address")) { + e.expected_address = parse_field_hex(entry["expected_address"].get()); + } else { + e.expected_address = FF::zero(); + } + entries.push_back(e); + } + + std::vector output_paths; + for (const auto& p : config["output_paths"]) { + output_paths.emplace_back(p.get()); + } + + auto keys = DefaultPublicKeys::instance(); + + std::vector> rows; + for (const auto& entry : entries) { + if (!std::filesystem::exists(entry.artifact_path)) { + info("Artifact not found: ", entry.artifact_path.string()); + return false; + } + auto art_content = read_file(entry.artifact_path); + auto artifact = nlohmann::json::parse(std::string(art_content.begin(), art_content.end())); + + auto bytecode = get_public_dispatch_bytecode(artifact); + FF bytecode_commitment = compute_public_bytecode_commitment(bytecode); + + FF class_id = poseidon2_hash_with_separator( + DOM_SEP__CONTRACT_CLASS_ID, { entry.artifact_hash, entry.private_functions_root, bytecode_commitment }); + + const nlohmann::json* init_fn = find_initializer_function(artifact); + FF init_hash = init_fn != nullptr ? compute_init_hash_empty_args(*init_fn) : FF::zero(); + + FF address = compute_contract_address(class_id, init_hash, entry.salt, entry.deployer, keys); + + info("Derived ", + entry.nr_const, + " from ", + entry.artifact_path.filename().string(), + ": ", + field_to_padded_hex(address)); + + if (!entry.expected_address.is_zero() && address != entry.expected_address) { + info("MISMATCH! Expected: ", field_to_padded_hex(entry.expected_address)); + info("Class ID: ", field_to_padded_hex(class_id)); + info("Public bytecode commit: ", field_to_padded_hex(bytecode_commitment)); + info("Init hash: ", field_to_padded_hex(init_hash)); + return false; + } + + rows.emplace_back(entry.nr_const, address); + } + + std::string content = render_noir_module(rows); + for (const auto& path : output_paths) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path); + out << content; + out.close(); + info("Wrote ", path.string()); + } + + return true; +} + +} // namespace bb +#endif diff --git a/barretenberg/cpp/src/barretenberg/api/standard_address_derivation.hpp b/barretenberg/cpp/src/barretenberg/api/standard_address_derivation.hpp new file mode 100644 index 000000000000..2341615fddbc --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/api/standard_address_derivation.hpp @@ -0,0 +1,48 @@ +#ifndef __wasm__ +#pragma once + +#include +#include +#include +#include + +#include "barretenberg/ecc/curves/bn254/fr.hpp" + +namespace bb { + +/** + * @brief Derive standard-contract addresses from freshly-compiled Noir artifacts and emit them + * as a `standard_addresses.nr` Noir module suitable for compilation against by other + * aztec-nr-using contracts. + * + * This is the C++ entrypoint for the `bb derive_standard_contract_addresses` subcommand, used + * by `noir-projects/noir-contracts/bootstrap.sh` to derive the auth_registry / public_checks / + * multi_call_entrypoint addresses BEFORE the second-phase Noir compile that bakes them into + * dependent contracts. Running address derivation here (in C++) instead of in TypeScript breaks + * the build-order chicken-and-egg: the TS generator needs `@aztec/stdlib`, which can't be built + * until after the Noir contracts compile, but the Noir contracts need the addresses to be + * baked-in before they compile. + * + * The config-file argument lists one entry per standard contract. Each entry names the freshly- + * compiled artifact, a precomputed `class_id_preimage` (whose `artifact_hash` and + * `private_functions_root` are provided by the TS generator — see "Known limitations" below), + * and the Noir constant name to emit. The same generated content is written to every path in + * `output_paths`, supporting the aztec/aztec_sublib twin layout without introducing a shared + * crate. + * + * Known limitations of this prototype: + * - `artifact_hash` and `private_functions_root` are READ from the config file rather than + * computed from the artifact. Porting their TS derivation (sha256-reduced-to-Fr merkle over + * selector + metadata hashes, plus deterministic-JSON-stringify of contract outputs) to C++ + * is mechanical but verbose — punted for a follow-up. The other primitives in the address + * derivation chain ARE computed in C++ from the artifact: public bytecode commitment from + * the freshly-built `public_dispatch` bytecode, contract class id, initialization hash, and + * the final partial-address-plus-curve-arithmetic step. + * + * @param config_path JSON config listing entries (see derive_standard_addresses.cpp for format) + * @return true on success, false on failure (parsing, hash derivation, file IO) + */ +bool derive_standard_contract_addresses(const std::filesystem::path& config_path); + +} // namespace bb +#endif diff --git a/barretenberg/cpp/src/barretenberg/bb/cli.cpp b/barretenberg/cpp/src/barretenberg/bb/cli.cpp index be1681019dd2..044143d19e67 100644 --- a/barretenberg/cpp/src/barretenberg/bb/cli.cpp +++ b/barretenberg/cpp/src/barretenberg/bb/cli.cpp @@ -21,6 +21,7 @@ #include "barretenberg/api/api_ultra_honk.hpp" #include "barretenberg/api/aztec_process.hpp" #include "barretenberg/api/file_io.hpp" +#include "barretenberg/api/standard_address_derivation.hpp" #include "barretenberg/bb/cli11_formatter.hpp" #include "barretenberg/bb/curve_constants.hpp" #include "barretenberg/bbapi/bbapi.hpp" @@ -722,6 +723,27 @@ int parse_and_run_cli_command(int argc, char* argv[]) add_verbose_flag(cache_paths_command); add_debug_flag(cache_paths_command); + /*************************************************************************************************************** + * Subcommand: derive_standard_contract_addresses + ***************************************************************************************************************/ + CLI::App* derive_standard_addresses_cmd = + app.add_subcommand("derive_standard_contract_addresses", + "Derive deployment addresses for the standard contracts (auth_registry, " + "public_checks, multi_call_entrypoint) from freshly-built Noir artifacts " + "and emit a `standard_addresses.nr` module for downstream Noir compilation. " + "Runs before the second phase of noir-contracts compilation in bootstrap."); + derive_standard_addresses_cmd->group(aztec_internal_group); + std::filesystem::path derive_standard_addresses_config; + derive_standard_addresses_cmd + ->add_option("--config", + derive_standard_addresses_config, + "JSON config listing each standard contract's artifact path, nr_const name, " + "precomputed artifact_hash, precomputed private_functions_root, and the output " + "paths to write the generated Noir module to.") + ->required(); + add_verbose_flag(derive_standard_addresses_cmd); + add_debug_flag(derive_standard_addresses_cmd); + /*************************************************************************************************************** * Subcommand: msgpack ***************************************************************************************************************/ @@ -919,6 +941,16 @@ int parse_and_run_cli_command(int argc, char* argv[]) return 0; } + // Derive standard contract addresses (used by noir-contracts bootstrap to bake addresses + // into aztec-nr-stamped `standard_addresses.nr` modules before the second compile phase). + if (derive_standard_addresses_cmd->parsed()) { +#ifdef __wasm__ + throw_or_abort("derive_standard_contract_addresses is not supported in WASM builds."); +#else + return derive_standard_contract_addresses(derive_standard_addresses_config) ? 0 : 1; +#endif + } + // MSGPACK if (msgpack_schema_command->parsed()) { std::cout << bbapi::get_msgpack_schema_as_json() << std::endl; diff --git a/noir-projects/noir-contracts/bootstrap.sh b/noir-projects/noir-contracts/bootstrap.sh index 7c8e941a4314..2ab511c42cd0 100755 --- a/noir-projects/noir-contracts/bootstrap.sh +++ b/noir-projects/noir-contracts/bootstrap.sh @@ -131,8 +131,64 @@ function compile { } export -f compile +# Standard contracts that get their addresses baked into `standard_addresses.nr` BEFORE other +# contracts compile against them. Listed by their `contracts/<...>` member name in Nargo.toml. +# Keep in sync with `yarn-project/standard-contracts/src/contract_data.ts`. +export STANDARD_CONTRACT_PATHS=( + "standard/auth_registry_contract" + "standard/public_checks_contract" + "standard/multi_call_entrypoint_contract" +) + +# Derive `standard_addresses.nr` for the standard contracts using their freshly-built artifacts. +# Reads the precomputed `artifact_hash` and `private_functions_root` (the TS-only hash inputs that +# the C++ port is still missing — see standard_address_derivation.cpp) from a sidecar JSON file +# pinned in standard-contracts. The C++ derives bytecode commitment, class id, init hash, and the +# final address. Output is written to BOTH the aztec-nr aztec crate AND its aztec_sublib twin so +# both compile sites see the same baked addresses. +function derive_standard_addresses { + local sidecar="../../yarn-project/standard-contracts/src/standard_contract_class_id_preimages.json" + if [ ! -f "$sidecar" ]; then + echo_stderr "Sidecar manifest missing: $sidecar" + echo_stderr "Run 'yarn workspace @aztec/standard-contracts run generate:data' to regenerate it" + echo_stderr "(or supply the artifact_hash / private_functions_root for each standard contract by hand)." + return 1 + fi + local config=$(mktemp) + # Compose the bb config from the pinned preimages + the artifacts that nargo just produced. + # The output paths are the aztec-nr/aztec twin layout. + jq -n \ + --arg ar_artifact ./target/auth_registry_contract-AuthRegistry.json \ + --arg pc_artifact ./target/public_checks_contract-PublicChecks.json \ + --slurpfile preimages "$sidecar" \ + '{ + entries: [ + { artifact_path: $ar_artifact, + nr_const: "STANDARD_AUTH_REGISTRY_ADDRESS", + artifact_hash: $preimages[0].AuthRegistry.artifactHash, + private_functions_root: $preimages[0].AuthRegistry.privateFunctionsRoot + }, + { artifact_path: $pc_artifact, + nr_const: "STANDARD_PUBLIC_CHECKS_ADDRESS", + artifact_hash: $preimages[0].PublicChecks.artifactHash, + private_functions_root: $preimages[0].PublicChecks.privateFunctionsRoot + } + ], + output_paths: [ + "../aztec-nr/aztec/src/standard_addresses.nr", + "./contracts/protocol/aztec_sublib/src/standard_addresses.nr" + ] + }' > "$config" + $BB derive_standard_contract_addresses --config "$config" + rm -f "$config" +} +export -f derive_standard_addresses + # If given an argument, it's the contract to compile. # Otherwise parse out all relevant contracts from the root Nargo.toml and process them in parallel. +# When no argument is provided, build in two phases: (1) compile the standard contracts, derive +# their addresses, stamp the addresses into `standard_addresses.nr`, then (2) compile the rest of +# the contracts against the now-fresh addresses. function build { echo_stderr "Compiling contracts (bb-hash: $BB_HASH)..." local folder_name @@ -145,24 +201,55 @@ function build { if [ "$#" -eq 0 ]; then rm -rf target mkdir -p target - local contracts=$(grep -oP "(?<=$folder_name/)[^\"]+" Nargo.toml) + local all_contracts=$(grep -oP "(?<=$folder_name/)[^\"]+" Nargo.toml) - # If a pinned standard-contracts archive is present, extract it into target/ and skip - # recompilation of those contracts. The archive is only committed on release branches; on - # next it is absent and this block is a no-op (everything compiles fresh). + # If a pinned standard-contracts archive is present, extract it into target/ and skip both + # phase-1 compilation and address derivation: the release branch ships standard_addresses.nr + # already in sync with the pinned artifacts. Absent on next; this block is a no-op there. + local skip_phase1=0 if [ -f pinned-standard-contracts.tar.gz ]; then echo_stderr "Using pinned-standard-contracts.tar.gz for pinned standard contracts." tar xzf pinned-standard-contracts.tar.gz -C target - contracts=$(echo "$contracts" | grep -vE "^standard/") + skip_phase1=1 + fi + + if [ $skip_phase1 -eq 0 ]; then + # Phase 1: standard contracts only. Compile sequentially-then-parallel so their artifacts + # exist before the derivation step. + set +e + parallel $PARALLEL_FLAGS --joblog joblog.txt -v --line-buffer --tag compile {} $folder_name \ + ::: ${STANDARD_CONTRACT_PATHS[@]} + code=$? + cat joblog.txt + [ $code -ne 0 ] && return $code + + # Derive addresses and update standard_addresses.nr in both aztec-nr/aztec and aztec_sublib. + derive_standard_addresses || return $? fi + + # Phase 2: all other contracts. Build the complement of standard contracts. + local other_contracts=() + for c in $all_contracts; do + local is_standard=0 + for std in ${STANDARD_CONTRACT_PATHS[@]}; do + [ "$c" = "$std" ] && { is_standard=1; break; } + done + [ $is_standard -eq 0 ] && other_contracts+=("$c") + done + set +e + parallel $PARALLEL_FLAGS --joblog joblog.txt -v --line-buffer --tag compile {} $folder_name \ + ::: ${other_contracts[@]} + code=$? + cat joblog.txt + return $code else local contracts="$@" + set +e + parallel $PARALLEL_FLAGS --joblog joblog.txt -v --line-buffer --tag compile {} $folder_name ::: ${contracts[@]} + code=$? + cat joblog.txt + return $code fi - set +e - parallel $PARALLEL_FLAGS --joblog joblog.txt -v --line-buffer --tag compile {} $folder_name ::: ${contracts[@]} - code=$? - cat joblog.txt - return $code } function test_cmds { diff --git a/yarn-project/standard-contracts/src/standard_contract_class_id_preimages.json b/yarn-project/standard-contracts/src/standard_contract_class_id_preimages.json new file mode 100644 index 000000000000..3410a4a56c34 --- /dev/null +++ b/yarn-project/standard-contracts/src/standard_contract_class_id_preimages.json @@ -0,0 +1,15 @@ +{ + "_comment": "Manifest of artifact_hash and private_functions_root for each standard contract. Consumed by `bb derive_standard_contract_addresses` from noir-contracts/bootstrap.sh during phase-1 compile, before the rest of noir-contracts compiles against the freshly-stamped addresses. Regenerated by `yarn workspace @aztec/standard-contracts run generate:data` whenever the TS chain produces different values (the existing drift check in generate_data.ts fails loudly if they fall behind). Long-term: port `computeArtifactHash` and `computePrivateFunctionsRoot` to C++ so this manifest becomes unnecessary.", + "AuthRegistry": { + "artifactHash": "0x303b31588973316c7544d070c05d148e628eed5a9fcd15c060f6251ea66e55fd", + "privateFunctionsRoot": "0x06e0363cea8d971d0c2988a13fc88774092b2858adc5ee0876ed2f9ad05e2f63" + }, + "MultiCallEntrypoint": { + "artifactHash": "0x111a5c01d590689e44fe17f639135cc1636393df6abadbaa832ceb02e6b17f4f", + "privateFunctionsRoot": "0x1497e05e2f823193577378ed04f91211331c657f7794113716315f52beff0a6a" + }, + "PublicChecks": { + "artifactHash": "0x2b11692497c9a3f1f44f4694d1ffb1e521d8cd6ba0e2ef3878026fe9c12fd854", + "privateFunctionsRoot": "0x202860adb1b8975971eeaf571aaaa88a27f4035290d58532ae7d60b0dfaad54c" + } +}