diff --git a/barretenberg/cpp/pil/vm2/constants_gen.pil b/barretenberg/cpp/pil/vm2/constants_gen.pil index 340ae4849682..d4944f4ef21a 100644 --- a/barretenberg/cpp/pil/vm2/constants_gen.pil +++ b/barretenberg/cpp/pil/vm2/constants_gen.pil @@ -15,10 +15,9 @@ namespace constants; pol MAX_L2_TO_L1_MSGS_PER_TX = 8; pol MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 3000; pol MAX_PROTOCOL_CONTRACTS = 11; + pol CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS = 1; pol CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS = 2; - pol CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS = 3; - pol MULTI_CALL_ENTRYPOINT_ADDRESS = 4; - pol FEE_JUICE_ADDRESS = 5; + pol FEE_JUICE_ADDRESS = 3; pol FEE_JUICE_BALANCES_SLOT = 1; pol UPDATED_CLASS_IDS_SLOT = 1; pol FLAT_PUBLIC_LOGS_HEADER_LENGTH = 1; diff --git a/barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp b/barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp index 9e59a9b4272d..4e63ef9ea318 100644 --- a/barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp @@ -21,10 +21,9 @@ #define GENESIS_ARCHIVE_ROOT "0x177a4955b31ecaafad999753938a44e526b54c5ba5d536688227f85f15cfbdf5" #define MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS 3000 #define MAX_PROTOCOL_CONTRACTS 11 +#define CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS 1 #define CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS 2 -#define CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS 3 -#define MULTI_CALL_ENTRYPOINT_ADDRESS 4 -#define FEE_JUICE_ADDRESS 5 +#define FEE_JUICE_ADDRESS 3 #define FEE_JUICE_BALANCES_SLOT 1 #define UPDATED_CLASS_IDS_SLOT 1 #define FLAT_PUBLIC_LOGS_HEADER_LENGTH 1 diff --git a/barretenberg/cpp/src/barretenberg/vm2/generated/relations/tx_impl.hpp b/barretenberg/cpp/src/barretenberg/vm2/generated/relations/tx_impl.hpp index 5a6610f87be7..3dad1fdb62c8 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/generated/relations/tx_impl.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/generated/relations/tx_impl.hpp @@ -19,7 +19,7 @@ void txImpl::accumulate(ContainerOverSubrelations& evals, const auto constants_MAX_NOTE_HASHES_PER_TX = FF(64); const auto constants_MAX_NULLIFIERS_PER_TX = FF(64); const auto constants_MAX_L2_TO_L1_MSGS_PER_TX = FF(8); - const auto constants_FEE_JUICE_ADDRESS = FF(5); + const auto constants_FEE_JUICE_ADDRESS = FF(3); const auto constants_FEE_JUICE_BALANCES_SLOT = FF(1); const auto constants_AVM_TX_PHASE_VALUE_START = FF(0); const auto constants_AVM_TX_PHASE_VALUE_LAST = FF(11); diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md b/docs/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md index 7455e8c529c9..3baf082e5a20 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md @@ -21,7 +21,7 @@ The `aztec` library includes authwit functionality. Import the necessary compone ```rust use aztec::{ - authwit::auth::{compute_authwit_message_hash_from_call, set_authorized}, + authwit::{utils::compute_authwit_message_hash_from_call, public::set_authorized}, macros::functions::authorize_once, }; ``` diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 5fba9d75ef01..329eaf9479aa 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -40,6 +40,14 @@ Aztec is in active development. Each version may introduce breaking changes that **Impact**: Code importing or referencing `ExtendedDirectionalAppTaggingSecret` should update to `AppTaggingSecret`. +### [Protocol] Remaining protocol-contract addresses compacted to 1-3 + +After the `auth_registry`, `public_checks`, and `multi_call_entrypoint` demotions freed up address slots `1`, `4`, and `6`, the three remaining protocol contracts have been compacted into the lowest slots: `ContractClassRegistry` moves from `3` to `1`, `ContractInstanceRegistry` stays at `2`, and `FeeJuice` moves from `5` to `3`. Code that hardcoded the previous values must be updated. `MAX_PROTOCOL_CONTRACTS` is unchanged (still `11`); only the assigned addresses moved. + +### [Aztec.nr] `multi_call_entrypoint` demoted from protocol contract + +`multi_call_entrypoint` is no longer a protocol contract. Its address is now derived from its artifact rather than hardcoded at `6`. PXE no longer auto-registers it; `EmbeddedWallet` registers it on creation via `registerMultiCallEntrypoint`. Other PXE consumers using `DefaultMultiCallEntrypoint` (including `DeployAccountMethod`'s `NO_FROM` self-deploy path) must register the contract themselves with `pxe.registerContract({ instance, artifact })` from `getStandardMultiCallEntrypoint()`. + ### [Aztec.nr] `public_checks` demoted from protocol contract `public_checks` is no longer a protocol contract. Its address is now derived from its artifact rather than hardcoded at `6`. The aztec-nr constant has moved and been renamed: diff --git a/docs/examples/contracts/example_uniswap/src/main.nr b/docs/examples/contracts/example_uniswap/src/main.nr index 90f1115c8ec0..60eb7dec3470 100644 --- a/docs/examples/contracts/example_uniswap/src/main.nr +++ b/docs/examples/contracts/example_uniswap/src/main.nr @@ -9,9 +9,9 @@ use aztec::macros::aztec; #[aztec] pub contract ExampleUniswap { use aztec::{ - authwit::auth::{ - assert_current_call_valid_authwit_public, compute_authwit_message_hash_from_call, - set_authorized, + authwit::{ + common::compute_authwit_message_hash_from_call, + public::{assert_current_call_valid_authwit_public, set_authorized}, }, macros::{functions::{external, initializer, only_self}, storage::storage}, protocol::{ diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index f5b2c48f3bfb..54d849e5893a 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -23,7 +23,7 @@ library Constants { 10_619_256_997_260_439_436_842_531_499_967_995_403_253_967_496_480_475_679_746_178_797_053_672_406_517; uint256 internal constant EMPTY_EPOCH_OUT_HASH = 355_785_372_471_781_095_838_790_036_702_437_931_769_306_153_278_986_832_745_847_530_947_941_691_539; - uint256 internal constant FEE_JUICE_ADDRESS = 5; + uint256 internal constant FEE_JUICE_ADDRESS = 3; uint256 internal constant BLS12_POINT_COMPRESSED_BYTES = 48; uint256 internal constant ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH = 111; uint256 internal constant NUM_MSGS_PER_BASE_PARITY = 256; diff --git a/noir-projects/aztec-nr/aztec/src/authwit/account.nr b/noir-projects/aztec-nr/aztec/src/authwit/account.nr index 72f5607f1392..e37df6cb1424 100644 --- a/noir-projects/aztec-nr/aztec/src/authwit/account.nr +++ b/noir-projects/aztec-nr/aztec/src/authwit/account.nr @@ -2,8 +2,8 @@ use crate::context::PrivateContext; use crate::protocol::{constants::DOM_SEP__TX_NULLIFIER, hash::poseidon2_hash_with_separator, traits::Hash}; -use crate::authwit::auth::{compute_authwit_message_hash, IS_VALID_SELECTOR}; use crate::authwit::entrypoint::app::AppPayload; +use crate::authwit::utils::{compute_authwit_message_hash, IS_VALID_SELECTOR}; pub struct AccountActions { context: Context, diff --git a/noir-projects/aztec-nr/aztec/src/authwit/auth.nr b/noir-projects/aztec-nr/aztec/src/authwit/auth.nr deleted file mode 100644 index f4564b99e84e..000000000000 --- a/noir-projects/aztec-nr/aztec/src/authwit/auth.nr +++ /dev/null @@ -1,422 +0,0 @@ -use crate::{ - authwit::{authorization_interface::AuthorizationInterface, AuthorizationSelector}, - context::{gas::GasOpts, PrivateContext, PublicContext}, - hash::hash_args, - macros::authorization::authorization, - oracle::{execution_cache::load, offchain_effect::emit_offchain_effect}, -}; -use crate::protocol::{ - abis::function_selector::FunctionSelector, - address::AztecAddress, - constants::{DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, DOM_SEP__AUTHWIT_OUTER}, - hash::poseidon2_hash_with_separator, - traits::{Serialize, ToField}, -}; -use crate::standard_addresses::STANDARD_AUTH_REGISTRY_ADDRESS; - -/// Authentication witness helper library -/// -/// Authentication Witness is a scheme for authenticating actions on Aztec, so users can allow third-parties (e.g. -/// protocols or other users) to execute an action on their behalf. -/// -/// This library provides helper functions to manage such witnesses. The authentication witness, is some "witness" -/// (data) that authenticates a `message_hash`. The simplest example of an authentication witness, is a signature. The -/// signature is the "evidence", that the signer has seen the message, agrees with it, and has allowed it. It does not -/// need to be a signature. It could be any kind of "proof" that the message is allowed. Another proof could be knowing -/// some kind of secret, or having some kind of "token" that allows the message. -/// -/// The `message_hash` is a hash of the following structure: hash(consumer, chain_id, version, inner_hash) -/// - consumer: the address of the contract that is "consuming" the message, -/// - chain_id: the chain id of the chain that the message is being consumed on, -/// - version: the version of the chain that the message is being consumed on, -/// - inner_hash: the hash of the "inner" message that is being consumed, this is the "actual" message or action. -/// -/// While the `inner_hash` could be anything, such as showing you signed a specific message, it will often be a hash of -/// the "action" to approve, along with who made the call. As part of this library, we provide a few helper functions -/// to deal with such messages. -/// -/// For example, we provide helper function that is used for checking that the message is an encoding of the current -/// call. This can be used to let some contract "allow" another contract to act on its behalf, as long as it can show -/// that it is acting on behalf of the contract. -/// -/// If we take a case of allowing a contract to transfer tokens on behalf of an account, the `inner_hash` can be -/// derived as: inner_hash = hash(caller, "transfer", hash(to, amount)) -/// -/// Where the `caller` would be the address of the contract that is trying to transfer the tokens, and `to` and -/// `amount` the arguments for the transfer. -/// -/// Note that we have both a `caller` and a `consumer`, the `consumer` will be the contract that is consuming the -/// message, in the case of the transfer, it would be the `Token` contract itself, while the caller, will be the actor -/// that is allowed to transfer the tokens. -/// -/// -/// The authentication mechanism works differently in public and private contexts. In private, we recall that -/// everything is executed on the user's device, so we can use `oracles` to "ask" the user (not contract) for -/// information. In public we cannot do this, since it is executed by the sequencer (someone else). Therefore we can -/// instead use a "registry" to store the messages that we have approved. -/// -/// A simple example would be a "token" that is being "pulled" from one account into another. We will first outline how -/// this would look in private, and then in public later. -/// -/// Say that a user `Alice` wants to deposit some tokens into a DeFi protocol (say a DEX). `Alice` would make a -/// `deposit` transaction, that she is executing using her account contract. The account would call the `DeFi` contract -/// to execute `deposit`, which would try to pull funds from the `Token` contract. Since the `DeFi` contract is trying -/// to pull funds from an account that is not its own, it needs to convince the `Token` contract that it is allowed to -/// do so. -/// -/// This is where the authentication witness comes in The `Token` contract computes a `message_hash` from the -/// `transfer` call, and then asks `Alice Account` contract to verify that the `DeFi` contract is allowed to execute -/// that call. -/// -/// `Alice Account` contract can then ask `Alice` if she wants to allow the `DeFi` contract to pull funds from her -/// account. If she does, she will sign the `message_hash` and return the signature to the `Alice Account` which will -/// validate it and return success to the `Token` contract which will then allow the `DeFi` contract to pull funds from -/// `Alice`. -/// -/// To ensure that the same "approval" cannot be used multiple times, we also compute a `nullifier` for the -/// authentication witness, and emit it from the `Token` contract (consumer). -/// -/// Note that we can do this flow as we are in private were we can do oracle calls out from contracts. -/// -/// -/// Person Contract Contract Contract -/// Alice Alice Account Token DeFi -/// | | | | -/// | Defi.deposit(Token, 1000) | | -/// |----------------->| | | -/// | | deposit(Token, 1000) | -/// | |---------------------------------------->| -/// | | | | -/// | | | transfer(Alice, Defi, 1000) -/// | | |<---------------------| -/// | | | | -/// | | Check if Defi may call transfer(Alice, Defi, 1000) -/// | |<-----------------| | -/// | | | | -/// | Please give me AuthWit for DeFi | | -/// | calling transfer(Alice, Defi, 1000) | | -/// |<-----------------| | | -/// | | | | -/// | | | | -/// | AuthWit for transfer(Alice, Defi, 1000) | -/// |----------------->| | | -/// | | AuthWit validity | | -/// | |----------------->| | -/// | | | | -/// | | throw if invalid AuthWit | -/// | | | | -/// | | emit AuthWit nullifier | -/// | | | | -/// | | transfer(Alice, Defi, 1000) | -/// | | | | -/// | | | | -/// | | | success | -/// | | |--------------------->| -/// | | | | -/// | | | | -/// | | | deposit(Token, 1000) -/// | | | | -/// | | | | -/// -/// -/// If we instead were in public, we cannot do the same flow. Instead we would use an authentication registry to store -/// the messages that we have approved. -/// -/// To approve a message, `Alice Account` can make a `set_authorized` call to the registry, to set a `message_hash` as -/// authorized. This is essentially a mapping from `message_hash` to `true` for `Alice Contract`. Every account has its -/// own map in the registry, so `Alice` cannot approve a message for `Bob`. -/// -/// The `Token` contract can then try to "spend" the approval by calling `consume` on the registry. If the message was -/// approved, the value is updated to `false`, and we return the success flag. For more information on the registry, -/// see `main.nr` in `auth_registry_contract`. -/// -/// Person Contract Contract Contract Contract -/// Alice Alice Account Registry Token DeFi -/// | | | | | -/// | Registry.set_authorized(..., true) | | | -/// |----------------->| | | | -/// | | set_authorized(..., true) | | -/// | |------------------->| | | -/// | | | | | -/// | | set authorized to true | | -/// | | | | | -/// | | | | | -/// | Defi.deposit(Token, 1000) | | | -/// |----------------->| | | | -/// | | deposit(Token, 1000) | | -/// | |-------------------------------------------------------------->| -/// | | | | | -/// | | | transfer(Alice, Defi, 1000) | -/// | | | |<---------------------| -/// | | | | | -/// | | | Check if Defi may call transfer(Alice, Defi, 1000) -/// | | |<------------------| | -/// | | | | | -/// | | throw if invalid AuthWit | | -/// | | | | | -/// | | | | | -/// | | set authorized to false | | -/// | | | | | -/// | | | | | -/// | | | AuthWit validity | | -/// | | |------------------>| | -/// | | | | | -/// | | | | transfer(Alice, Defi, 1000) -/// | | | |<-------------------->| -/// | | | | | -/// | | | | success | -/// | | | |--------------------->| -/// | | | | | -/// | | | | deposit(Token, 1000) -/// | | | | | -/// -/// -/// --- FAQ --- -/// Q: Why are we using a success flag of `poseidon2_hash_bytes("IS_VALID()")` instead of just returning a boolean? -/// A: We want to make sure that we don't accidentally return `true` if there is a collision in the function -/// selector. By returning a hash of `IS_VALID()`, it becomes very unlikely that there is both a collision and we -/// return a success flag. -/// -/// Q: Why are we using static calls? -/// A: We are using static calls to ensure that the account contract cannot re-enter. If it was a normal call, it -/// could make a new call and do a re-entry attack. Using a static ensures that it cannot update any state. -/// -/// Q: Would it not be cheaper to use a nullifier instead of updating state in public? -/// A: At a quick glance, a public state update + nullifier is 96 bytes, but two state updates are 128, so it would -/// be cheaper to use a nullifier, if this is the way it would always be done. However, if both the approval and the -/// consumption is done in the same transaction, then we will be able to squash the updates (only final tx state diff -/// is posted to DA), and now it is cheaper. -/// -/// Q: Why is the chain id and the version part of the message hash? -/// A: The chain id and the version is part of the message hash to ensure that the message is only valid on a -/// specific chain to avoid a case where the same message could be used across multiple chains. - -pub global IS_VALID_SELECTOR: Field = 0x47dacd73; // 4 last bytes of -// poseidon2_hash_bytes("IS_VALID()") - -/// A struct that represents a contract call the user can authorize. It's associated identifier is generated by -/// serializing and hashing it. The user is expected to sign this hash to signal the contract call can be performed on -/// their behalf -#[authorization] -struct CallAuthorization { - msg_sender: AztecAddress, - selector: FunctionSelector, - args_hash: Field, -} - -/// A struct that represents a request to authorize a call, which is used to emit an offchain effect so the user/wallet -/// can understand what they are being asked to sign. It is generated from a CallAuthorization by adding metadata to -/// it, such as the selector for the authorization, the inner hash, and the actual arguments that are being passed to -/// the function call. -#[derive(Serialize)] -struct CallAuthorizationRequest { - selector: AuthorizationSelector, - inner_hash: Field, - on_behalf_of: AztecAddress, - msg_sender: AztecAddress, - fn_selector: FunctionSelector, - args_hash: Field, -} - -unconstrained fn emit_authorization_as_offchain_effect( - authorization: CallAuthorization, - inner_hash: Field, - on_behalf_of: AztecAddress, -) { - let args: [Field; N] = load(authorization.args_hash); - let authorization_request = CallAuthorizationRequest { - selector: authorization.get_authorization_selector(), - inner_hash: inner_hash, - on_behalf_of: on_behalf_of, - msg_sender: authorization.msg_sender, - fn_selector: authorization.selector, - args_hash: authorization.args_hash, - }; - emit_offchain_effect(authorization_request.serialize().concat(args)) -} - -/// Assert that `on_behalf_of` has authorized the current call with a valid authentication witness -/// -/// Compute the `inner_hash` using the `msg_sender`, `selector` and `args_hash` and then make a call out to the -/// `on_behalf_of` contract to verify that the `inner_hash` is valid. -/// -/// Additionally, this function emits the identifying information of the call as an offchain effect so PXE can rely the -/// information to the user/wallet in a readable way. To that effect, it is generic over N, where N is the number of -/// arguments the authorized functions takes. This is used to load the arguments from the execution cache. This -/// function is intended to be called via a macro, which will use the turbofish operator to specify the number of -/// arguments. -/// -/// @param on_behalf_of The address that has allegedly authorized the current call -pub fn assert_current_call_valid_authwit(context: &mut PrivateContext, on_behalf_of: AztecAddress) { - let args_hash: Field = context.get_args_hash(); - - let authorization = - CallAuthorization { msg_sender: context.maybe_msg_sender().unwrap(), selector: context.selector(), args_hash }; - let inner_hash = compute_inner_authwit_hash(authorization.serialize()); - // Safety: Offchain effects are by definition unconstrained. They are emitted via an oracle which we don't use for - // anything besides its side effects, therefore this is safe to call. - unsafe { emit_authorization_as_offchain_effect::(authorization, inner_hash, on_behalf_of) }; - - assert_inner_hash_valid_authwit(context, on_behalf_of, inner_hash); -} - -/// Assert that a specific `inner_hash` is valid for the `on_behalf_of` address -/// -/// Used as an internal function for `assert_current_call_valid_authwit` and can be used as a standalone function when -/// the `inner_hash` is from a different source, e.g., say a block of text etc. -/// -/// @param on_behalf_of The address that has allegedly authorized the current call @param inner_hash The hash of the -/// message to authorize -pub fn assert_inner_hash_valid_authwit(context: &mut PrivateContext, on_behalf_of: AztecAddress, inner_hash: Field) { - // We perform a static call here and not a standard one to ensure that the account contract cannot re-enter. - let result: Field = context - .static_call_private_function( - on_behalf_of, - comptime { FunctionSelector::from_signature("verify_private_authwit(Field)") }, - [inner_hash], - ) - .get_preimage(); - assert(result == IS_VALID_SELECTOR, "Message not authorized by account"); - // Compute the nullifier, similar computation to the outer hash, but without the chain_id and version. Those should - // already be handled in the verification, so we just need something to nullify, that allows the same inner_hash - // for multiple actors. - let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); - context.push_nullifier_unsafe(nullifier); -} - -/// Assert that `on_behalf_of` has authorized the current call in the authentication registry -/// -/// Compute the `inner_hash` using the `msg_sender`, `selector` and `args_hash` and then make a call out to the -/// `on_behalf_of` contract to verify that the `inner_hash` is valid. -/// -/// Note that the authentication registry will take the `msg_sender` into account as the consumer, so this will only -/// work if the `msg_sender` is the same as the `consumer` when the `message_hash` was inserted into the registry. -/// -/// @param on_behalf_of The address that has allegedly authorized the current call -pub unconstrained fn assert_current_call_valid_authwit_public(context: PublicContext, on_behalf_of: AztecAddress) { - let inner_hash = compute_inner_authwit_hash([ - context.maybe_msg_sender().unwrap().to_field(), - context.selector().to_field(), - context.get_args_hash(), - ]); - assert_inner_hash_valid_authwit_public(context, on_behalf_of, inner_hash); -} - -/// Assert that `on_behalf_of` has authorized a specific `inner_hash` in the authentication registry -/// -/// Compute the `inner_hash` using the `msg_sender`, `selector` and `args_hash` and then make a call out to the -/// `on_behalf_of` contract to verify that the `inner_hash` is valid. -/// -/// Note that the authentication registry will take the `msg_sender` into account as the consumer, so this will only -/// work if the `msg_sender` is the same as the `consumer` when the `message_hash` was inserted into the registry. -/// -/// @param on_behalf_of The address that has allegedly authorized the `inner_hash` -pub unconstrained fn assert_inner_hash_valid_authwit_public( - context: PublicContext, - on_behalf_of: AztecAddress, - inner_hash: Field, -) { - let results: [Field] = context.call_public_function( - STANDARD_AUTH_REGISTRY_ADDRESS, - comptime { FunctionSelector::from_signature("consume((Field),Field)") }, - [on_behalf_of.to_field(), inner_hash], - GasOpts::default(), - ); - assert(results.len() == 1, "Invalid response from registry"); - assert(results[0] == IS_VALID_SELECTOR, "Message not authorized by account"); -} - -/// Compute the `message_hash` from a function call to be used by an authentication witness -/// -/// Useful for when you need a non-account contract to approve during execution. For example if you need a contract to -/// make a call to nested contract, e.g., contract A wants to exit token T to L1 using bridge B, so it needs to allow B -/// to transfer T on its behalf. -/// -/// @param caller The address of the contract that is calling the function, in the example above, this would be B -/// @param consumer The address of the contract that is consuming the message, in the example above, this would be T -/// @param chain_id The chain id of the chain that the message is being consumed on @param version The version of the -/// chain that the message is being consumed on @param selector The function selector of the function that is being -/// called @param args The arguments of the function that is being called -pub fn compute_authwit_message_hash_from_call( - caller: AztecAddress, - consumer: AztecAddress, - chain_id: Field, - version: Field, - selector: FunctionSelector, - args: [Field; N], -) -> Field { - let args_hash = hash_args(args); - let inner_hash = compute_inner_authwit_hash([caller.to_field(), selector.to_field(), args_hash]); - compute_authwit_message_hash(consumer, chain_id, version, inner_hash) -} - -/// Computes the `inner_hash` of the authentication witness -/// -/// This is used internally, but also useful in cases where you want to compute the `inner_hash` for a specific message -/// that is not necessarily a call, but just some "bytes" or text. -/// -/// @param args The arguments to hash -pub fn compute_inner_authwit_hash(args: [Field; N]) -> Field { - poseidon2_hash_with_separator(args, DOM_SEP__AUTHWIT_INNER) -} - -/// Computes the `authwit_nullifier` for a specific `on_behalf_of` and `inner_hash` -/// -/// Using the `on_behalf_of` and the `inner_hash` to ensure that the nullifier is siloed for a specific `on_behalf_of`. -/// -/// @param on_behalf_of The address that has authorized the `inner_hash` @param inner_hash The hash of the message to -/// authorize -pub fn compute_authwit_nullifier(on_behalf_of: AztecAddress, inner_hash: Field) -> Field { - poseidon2_hash_with_separator( - [on_behalf_of.to_field(), inner_hash], - DOM_SEP__AUTHWIT_NULLIFIER, - ) -} - -/// Computes the `message_hash` for the authentication witness -/// -/// @param consumer The address of the contract that is consuming the message @param chain_id The chain id of the chain -/// that the message is being consumed on @param version The version of the chain that the message is being consumed on -/// @param inner_hash The hash of the "inner" message that is being consumed -pub fn compute_authwit_message_hash( - consumer: AztecAddress, - chain_id: Field, - version: Field, - inner_hash: Field, -) -> Field { - poseidon2_hash_with_separator( - [consumer.to_field(), chain_id, version, inner_hash], - DOM_SEP__AUTHWIT_OUTER, - ) -} - -/// Helper function to set the authorization status of a message hash -/// -/// Wraps a public call to the authentication registry to set the authorization status of a `message_hash` -/// -/// @param message_hash The hash of the message to authorize @param authorize True if the message should be authorized, -/// false if it should be revoked -pub unconstrained fn set_authorized(context: PublicContext, message_hash: Field, authorize: bool) { - let res = context.call_public_function( - STANDARD_AUTH_REGISTRY_ADDRESS, - comptime { FunctionSelector::from_signature("set_authorized(Field,bool)") }, - [message_hash, authorize as Field], - GasOpts::default(), - ); - assert(res.len() == 0); -} - -/// Helper function to reject all authwits -/// -/// Wraps a public call to the authentication registry to set the `reject_all` flag -/// -/// @param reject True if all authwits should be rejected, false otherwise -pub unconstrained fn set_reject_all(context: PublicContext, reject: bool) { - let res = context.call_public_function( - STANDARD_AUTH_REGISTRY_ADDRESS, - comptime { FunctionSelector::from_signature("set_reject_all(bool)") }, - [reject as Field], - GasOpts::default(), - ); - assert(res.len() == 0); -} diff --git a/noir-projects/aztec-nr/aztec/src/authwit/mod.nr b/noir-projects/aztec-nr/aztec/src/authwit/mod.nr index a403658ef642..00db21367f6b 100644 --- a/noir-projects/aztec-nr/aztec/src/authwit/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/authwit/mod.nr @@ -4,5 +4,7 @@ pub mod account; pub mod authorization_interface; mod authorization_selector; pub use authorization_selector::AuthorizationSelector; -pub mod auth; +pub mod utils; +pub mod private; +pub mod public; pub mod entrypoint; diff --git a/noir-projects/aztec-nr/aztec/src/authwit/private.nr b/noir-projects/aztec-nr/aztec/src/authwit/private.nr new file mode 100644 index 000000000000..b1344dbadb1b --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/authwit/private.nr @@ -0,0 +1,117 @@ +use crate::{ + authwit::utils::{ + CallAuthorization, compute_authwit_nullifier, compute_inner_authwit_hash, emit_authorization_as_offchain_effect, + IS_VALID_SELECTOR, + }, + context::PrivateContext, +}; +use crate::protocol::{abis::function_selector::FunctionSelector, address::AztecAddress, traits::Serialize}; + +/// Private-flow authwit helpers. +/// +/// Say that a user `Alice` wants to deposit some tokens into a DeFi protocol (say a DEX). `Alice` would make a +/// `deposit` transaction, that she is executing using her account contract. The account would call the `DeFi` +/// contract to execute `deposit`, which would try to pull funds from the `Token` contract. Since the `DeFi` contract +/// is trying to pull funds from an account that is not its own, it needs to convince the `Token` contract that it is +/// allowed to do so. +/// +/// This is where the authentication witness comes in. The `Token` contract computes a `message_hash` from the +/// `transfer` call, and then asks `Alice Account` contract to verify that the `DeFi` contract is allowed to execute +/// that call. +/// +/// `Alice Account` contract can then ask `Alice` if she wants to allow the `DeFi` contract to pull funds from her +/// account. If she does, she will sign the `message_hash` and return the signature to the `Alice Account` which will +/// validate it and return success to the `Token` contract which will then allow the `DeFi` contract to pull funds +/// from `Alice`. +/// +/// To ensure that the same "approval" cannot be used multiple times, we also compute a `nullifier` for the +/// authentication witness, and emit it from the `Token` contract (consumer). +/// +/// Note that we can do this flow as we are in private where we can do oracle calls out from contracts. +/// +/// Person Contract Contract Contract +/// Alice Alice Account Token DeFi +/// | | | | +/// | Defi.deposit(Token, 1000) | | +/// |----------------->| | | +/// | | deposit(Token, 1000) | +/// | |---------------------------------------->| +/// | | | | +/// | | | transfer(Alice, Defi, 1000) +/// | | |<---------------------| +/// | | | | +/// | | Check if Defi may call transfer(Alice, Defi, 1000) +/// | |<-----------------| | +/// | | | | +/// | Please give me AuthWit for DeFi | | +/// | calling transfer(Alice, Defi, 1000) | | +/// |<-----------------| | | +/// | | | | +/// | | | | +/// | AuthWit for transfer(Alice, Defi, 1000) | +/// |----------------->| | | +/// | | AuthWit validity | | +/// | |----------------->| | +/// | | | | +/// | | throw if invalid AuthWit | +/// | | | | +/// | | emit AuthWit nullifier | +/// | | | | +/// | | transfer(Alice, Defi, 1000) | +/// | | | | +/// | | | | +/// | | | success | +/// | | |--------------------->| +/// | | | | +/// | | | | +/// | | | deposit(Token, 1000) +/// | | | | + +/// Assert that `on_behalf_of` has authorized the current call with a valid authentication witness +/// +/// Compute the `inner_hash` using the `msg_sender`, `selector` and `args_hash` and then make a call out to the +/// `on_behalf_of` contract to verify that the `inner_hash` is valid. +/// +/// Additionally, this function emits the identifying information of the call as an offchain effect so PXE can rely the +/// information to the user/wallet in a readable way. To that effect, it is generic over N, where N is the number of +/// arguments the authorized functions takes. This is used to load the arguments from the execution cache. This +/// function is intended to be called via a macro, which will use the turbofish operator to specify the number of +/// arguments. +/// +/// @param on_behalf_of The address that has allegedly authorized the current call +pub fn assert_current_call_valid_authwit(context: &mut PrivateContext, on_behalf_of: AztecAddress) { + let args_hash: Field = context.get_args_hash(); + + let authorization = + CallAuthorization { msg_sender: context.maybe_msg_sender().unwrap(), selector: context.selector(), args_hash }; + let inner_hash = compute_inner_authwit_hash(authorization.serialize()); + // Safety: Offchain effects are by definition unconstrained. They are emitted via an oracle which we don't use for + // anything besides its side effects, therefore this is safe to call. + unsafe { emit_authorization_as_offchain_effect::(authorization, inner_hash, on_behalf_of) }; + + assert_inner_hash_valid_authwit(context, on_behalf_of, inner_hash); +} + +/// Assert that a specific `inner_hash` is valid for the `on_behalf_of` address +/// +/// Used as an internal function for `assert_current_call_valid_authwit` and can be used as a standalone function when +/// the `inner_hash` is from a different source, e.g., say a block of text etc. +/// +/// @param on_behalf_of The address that has allegedly authorized the current call @param inner_hash The hash of the +/// message to authorize +pub fn assert_inner_hash_valid_authwit(context: &mut PrivateContext, on_behalf_of: AztecAddress, inner_hash: Field) { + // We perform a static call here and not a standard one to ensure that the account contract cannot re-enter. + let result: Field = context + .static_call_private_function( + on_behalf_of, + comptime { FunctionSelector::from_signature("verify_private_authwit(Field)") }, + [inner_hash], + ) + .get_preimage(); + assert(result == IS_VALID_SELECTOR, "Message not authorized by account"); + // Compute the nullifier, similar computation to the outer hash, but without the chain_id and version. Those should + // already be handled in the verification, so we just need something to nullify, that allows the same inner_hash + // for multiple actors. + let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); + context.push_nullifier(nullifier); +} diff --git a/noir-projects/aztec-nr/aztec/src/authwit/public.nr b/noir-projects/aztec-nr/aztec/src/authwit/public.nr new file mode 100644 index 000000000000..20db1a32d5a9 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/authwit/public.nr @@ -0,0 +1,128 @@ +use crate::{authwit::utils::{compute_inner_authwit_hash, IS_VALID_SELECTOR}, context::{gas::GasOpts, PublicContext}}; +use crate::protocol::{abis::function_selector::FunctionSelector, address::AztecAddress, traits::ToField}; +use crate::standard_addresses::STANDARD_AUTH_REGISTRY_ADDRESS; + +/// Public-flow authwit helpers. +/// +/// In public, we cannot use oracles to "ask" the user for an authentication witness like we do in private, since the +/// public execution is performed by the sequencer. Instead, an account uses a "registry" to record the messages they +/// have approved. To approve a message, `Alice Account` makes a `set_authorized` call to the registry, recording a +/// `message_hash -> true` mapping for `Alice Contract`. Every account has its own map in the registry, so `Alice` +/// cannot approve a message for `Bob`. +/// +/// The `Token` contract can then try to "spend" the approval by calling `consume` on the registry. If the message was +/// approved, the value is updated to `false`, and we return the success flag. For more information on the registry, +/// see `main.nr` in `auth_registry_contract`. +/// +/// Person Contract Contract Contract Contract +/// Alice Alice Account Registry Token DeFi +/// | | | | | +/// | Registry.set_authorized(..., true) | | | +/// |----------------->| | | | +/// | | set_authorized(..., true) | | +/// | |------------------->| | | +/// | | | | | +/// | | set authorized to true | | +/// | | | | | +/// | | | | | +/// | Defi.deposit(Token, 1000) | | | +/// |----------------->| | | | +/// | | deposit(Token, 1000) | | +/// | |-------------------------------------------------------------->| +/// | | | | | +/// | | | transfer(Alice, Defi, 1000) | +/// | | | |<---------------------| +/// | | | | | +/// | | | Check if Defi may call transfer(Alice, Defi, 1000) +/// | | |<------------------| | +/// | | | | | +/// | | throw if invalid AuthWit | | +/// | | | | | +/// | | | | | +/// | | set authorized to false | | +/// | | | | | +/// | | | | | +/// | | | AuthWit validity | | +/// | | |------------------>| | +/// | | | | | +/// | | | | transfer(Alice, Defi, 1000) +/// | | | |<-------------------->| +/// | | | | | +/// | | | | success | +/// | | | |--------------------->| +/// | | | | | +/// | | | | deposit(Token, 1000) +/// | | | | | + +/// Assert that `on_behalf_of` has authorized the current call in the authentication registry +/// +/// Compute the `inner_hash` using the `msg_sender`, `selector` and `args_hash` and then make a call out to the +/// `on_behalf_of` contract to verify that the `inner_hash` is valid. +/// +/// Note that the authentication registry will take the `msg_sender` into account as the consumer, so this will only +/// work if the `msg_sender` is the same as the `consumer` when the `message_hash` was inserted into the registry. +/// +/// @param on_behalf_of The address that has allegedly authorized the current call +pub unconstrained fn assert_current_call_valid_authwit_public(context: PublicContext, on_behalf_of: AztecAddress) { + let inner_hash = compute_inner_authwit_hash([ + context.maybe_msg_sender().unwrap().to_field(), + context.selector().to_field(), + context.get_args_hash(), + ]); + assert_inner_hash_valid_authwit_public(context, on_behalf_of, inner_hash); +} + +/// Assert that `on_behalf_of` has authorized a specific `inner_hash` in the authentication registry +/// +/// Compute the `inner_hash` using the `msg_sender`, `selector` and `args_hash` and then make a call out to the +/// `on_behalf_of` contract to verify that the `inner_hash` is valid. +/// +/// Note that the authentication registry will take the `msg_sender` into account as the consumer, so this will only +/// work if the `msg_sender` is the same as the `consumer` when the `message_hash` was inserted into the registry. +/// +/// @param on_behalf_of The address that has allegedly authorized the `inner_hash` +pub unconstrained fn assert_inner_hash_valid_authwit_public( + context: PublicContext, + on_behalf_of: AztecAddress, + inner_hash: Field, +) { + let results: [Field] = context.call_public_function( + STANDARD_AUTH_REGISTRY_ADDRESS, + comptime { FunctionSelector::from_signature("consume((Field),Field)") }, + [on_behalf_of.to_field(), inner_hash], + GasOpts::default(), + ); + assert(results.len() == 1, "Invalid response from registry"); + assert(results[0] == IS_VALID_SELECTOR, "Message not authorized by account"); +} + +/// Helper function to set the authorization status of a message hash +/// +/// Wraps a public call to the authentication registry to set the authorization status of a `message_hash` +/// +/// @param message_hash The hash of the message to authorize @param authorize True if the message should be authorized, +/// false if it should be revoked +pub unconstrained fn set_authorized(context: PublicContext, message_hash: Field, authorize: bool) { + let res = context.call_public_function( + STANDARD_AUTH_REGISTRY_ADDRESS, + comptime { FunctionSelector::from_signature("set_authorized(Field,bool)") }, + [message_hash, authorize as Field], + GasOpts::default(), + ); + assert(res.len() == 0); +} + +/// Helper function to reject all authwits +/// +/// Wraps a public call to the authentication registry to set the `reject_all` flag +/// +/// @param reject True if all authwits should be rejected, false otherwise +pub unconstrained fn set_reject_all(context: PublicContext, reject: bool) { + let res = context.call_public_function( + STANDARD_AUTH_REGISTRY_ADDRESS, + comptime { FunctionSelector::from_signature("set_reject_all(bool)") }, + [reject as Field], + GasOpts::default(), + ); + assert(res.len() == 0); +} diff --git a/noir-projects/aztec-nr/aztec/src/authwit/utils.nr b/noir-projects/aztec-nr/aztec/src/authwit/utils.nr new file mode 100644 index 000000000000..bcab20e9cde4 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/authwit/utils.nr @@ -0,0 +1,183 @@ +use crate::{ + authwit::{authorization_interface::AuthorizationInterface, AuthorizationSelector}, + hash::hash_args, + macros::authorization::authorization, + oracle::{execution_cache::load, offchain_effect::emit_offchain_effect}, +}; +use crate::protocol::{ + abis::function_selector::FunctionSelector, + address::AztecAddress, + constants::{DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, DOM_SEP__AUTHWIT_OUTER}, + hash::poseidon2_hash_with_separator, + traits::{Serialize, ToField}, +}; + +/// Authentication witness helper library +/// +/// Authentication Witness is a scheme for authenticating actions on Aztec, so users can allow third-parties (e.g. +/// protocols or other users) to execute an action on their behalf. +/// +/// This library provides helper functions to manage such witnesses. The authentication witness, is some "witness" +/// (data) that authenticates a `message_hash`. The simplest example of an authentication witness, is a signature. The +/// signature is the "evidence", that the signer has seen the message, agrees with it, and has allowed it. It does not +/// need to be a signature. It could be any kind of "proof" that the message is allowed. Another proof could be knowing +/// some kind of secret, or having some kind of "token" that allows the message. +/// +/// The `message_hash` is a hash of the following structure: hash(consumer, chain_id, version, inner_hash) +/// - consumer: the address of the contract that is "consuming" the message, +/// - chain_id: the chain id of the chain that the message is being consumed on, +/// - version: the version of the chain that the message is being consumed on, +/// - inner_hash: the hash of the "inner" message that is being consumed, this is the "actual" message or action. +/// +/// While the `inner_hash` could be anything, such as showing you signed a specific message, it will often be a hash of +/// the "action" to approve, along with who made the call. As part of this library, we provide a few helper functions +/// to deal with such messages. +/// +/// For example, we provide helper function that is used for checking that the message is an encoding of the current +/// call. This can be used to let some contract "allow" another contract to act on its behalf, as long as it can show +/// that it is acting on behalf of the contract. +/// +/// If we take a case of allowing a contract to transfer tokens on behalf of an account, the `inner_hash` can be +/// derived as: inner_hash = hash(caller, "transfer", hash(to, amount)) +/// +/// Where the `caller` would be the address of the contract that is trying to transfer the tokens, and `to` and +/// `amount` the arguments for the transfer. +/// +/// Note that we have both a `caller` and a `consumer`, the `consumer` will be the contract that is consuming the +/// message, in the case of the transfer, it would be the `Token` contract itself, while the caller, will be the actor +/// that is allowed to transfer the tokens. +/// +/// The authentication mechanism works differently in public and private contexts. In private, we recall that +/// everything is executed on the user's device, so we can use `oracles` to "ask" the user (not contract) for +/// information. In public we cannot do this, since it is executed by the sequencer (someone else). Therefore we can +/// instead use a "registry" to store the messages that we have approved. See `authwit::private` and `authwit::public` +/// for the corresponding flow-specific helpers; the symbols defined here are address-independent crypto utilities +/// shared by both paths. +/// +/// --- FAQ --- +/// Q: Why are we using a success flag of `poseidon2_hash_bytes("IS_VALID()")` instead of just returning a boolean? +/// A: We want to make sure that we don't accidentally return `true` if there is a collision in the function +/// selector. By returning a hash of `IS_VALID()`, it becomes very unlikely that there is both a collision and we +/// return a success flag. +/// +/// Q: Why are we using static calls? +/// A: We are using static calls to ensure that the account contract cannot re-enter. If it was a normal call, it +/// could make a new call and do a re-entry attack. Using a static ensures that it cannot update any state. +/// +/// Q: Would it not be cheaper to use a nullifier instead of updating state in public? +/// A: At a quick glance, a public state update + nullifier is 96 bytes, but two state updates are 128, so it would +/// be cheaper to use a nullifier, if this is the way it would always be done. However, if both the approval and the +/// consumption is done in the same transaction, then we will be able to squash the updates (only final tx state diff +/// is posted to DA), and now it is cheaper. +/// +/// Q: Why is the chain id and the version part of the message hash? +/// A: The chain id and the version is part of the message hash to ensure that the message is only valid on a +/// specific chain to avoid a case where the same message could be used across multiple chains. + +pub global IS_VALID_SELECTOR: Field = 0x47dacd73; // 4 last bytes of +// poseidon2_hash_bytes("IS_VALID()") + +/// A struct that represents a contract call the user can authorize. It's associated identifier is generated by +/// serializing and hashing it. The user is expected to sign this hash to signal the contract call can be performed on +/// their behalf +#[authorization] +pub struct CallAuthorization { + pub msg_sender: AztecAddress, + pub selector: FunctionSelector, + pub args_hash: Field, +} + +/// A struct that represents a request to authorize a call, which is used to emit an offchain effect so the user/wallet +/// can understand what they are being asked to sign. It is generated from a CallAuthorization by adding metadata to +/// it, such as the selector for the authorization, the inner hash, and the actual arguments that are being passed to +/// the function call. +#[derive(Serialize)] +pub struct CallAuthorizationRequest { + pub selector: AuthorizationSelector, + pub inner_hash: Field, + pub on_behalf_of: AztecAddress, + pub msg_sender: AztecAddress, + pub fn_selector: FunctionSelector, + pub args_hash: Field, +} + +pub(crate) unconstrained fn emit_authorization_as_offchain_effect( + authorization: CallAuthorization, + inner_hash: Field, + on_behalf_of: AztecAddress, +) { + let args: [Field; N] = load(authorization.args_hash); + let authorization_request = CallAuthorizationRequest { + selector: authorization.get_authorization_selector(), + inner_hash: inner_hash, + on_behalf_of: on_behalf_of, + msg_sender: authorization.msg_sender, + fn_selector: authorization.selector, + args_hash: authorization.args_hash, + }; + emit_offchain_effect(authorization_request.serialize().concat(args)) +} + +/// Compute the `message_hash` from a function call to be used by an authentication witness +/// +/// Useful for when you need a non-account contract to approve during execution. For example if you need a contract to +/// make a call to nested contract, e.g., contract A wants to exit token T to L1 using bridge B, so it needs to allow B +/// to transfer T on its behalf. +/// +/// @param caller The address of the contract that is calling the function, in the example above, this would be B +/// @param consumer The address of the contract that is consuming the message, in the example above, this would be T +/// @param chain_id The chain id of the chain that the message is being consumed on @param version The version of the +/// chain that the message is being consumed on @param selector The function selector of the function that is being +/// called @param args The arguments of the function that is being called +pub fn compute_authwit_message_hash_from_call( + caller: AztecAddress, + consumer: AztecAddress, + chain_id: Field, + version: Field, + selector: FunctionSelector, + args: [Field; N], +) -> Field { + let args_hash = hash_args(args); + let inner_hash = compute_inner_authwit_hash([caller.to_field(), selector.to_field(), args_hash]); + compute_authwit_message_hash(consumer, chain_id, version, inner_hash) +} + +/// Computes the `inner_hash` of the authentication witness +/// +/// This is used internally, but also useful in cases where you want to compute the `inner_hash` for a specific message +/// that is not necessarily a call, but just some "bytes" or text. +/// +/// @param args The arguments to hash +pub fn compute_inner_authwit_hash(args: [Field; N]) -> Field { + poseidon2_hash_with_separator(args, DOM_SEP__AUTHWIT_INNER) +} + +/// Computes the `authwit_nullifier` for a specific `on_behalf_of` and `inner_hash` +/// +/// Using the `on_behalf_of` and the `inner_hash` to ensure that the nullifier is siloed for a specific `on_behalf_of`. +/// +/// @param on_behalf_of The address that has authorized the `inner_hash` @param inner_hash The hash of the message to +/// authorize +pub fn compute_authwit_nullifier(on_behalf_of: AztecAddress, inner_hash: Field) -> Field { + poseidon2_hash_with_separator( + [on_behalf_of.to_field(), inner_hash], + DOM_SEP__AUTHWIT_NULLIFIER, + ) +} + +/// Computes the `message_hash` for the authentication witness +/// +/// @param consumer The address of the contract that is consuming the message @param chain_id The chain id of the chain +/// that the message is being consumed on @param version The version of the chain that the message is being consumed on +/// @param inner_hash The hash of the "inner" message that is being consumed +pub fn compute_authwit_message_hash( + consumer: AztecAddress, + chain_id: Field, + version: Field, + inner_hash: Field, +) -> Field { + poseidon2_hash_with_separator( + [consumer.to_field(), chain_id, version, inner_hash], + DOM_SEP__AUTHWIT_OUTER, + ) +} diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr index 71e4341f8afa..40cbd1cc8c9b 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/helpers.nr @@ -104,9 +104,9 @@ pub(crate) comptime fn create_authorize_once_check(f: FunctionDefinition, is_pri .join(quote {+}); quote { ($params_quote_without_parentheses) } }; - quote { aztec::authwit::auth::assert_current_call_valid_authwit::<($serialized_len_quote)> } + quote { aztec::authwit::private::assert_current_call_valid_authwit::<($serialized_len_quote)> } } else { - quote { aztec::authwit::auth::assert_current_call_valid_authwit_public } + quote { aztec::authwit::public::assert_current_call_valid_authwit_public } }; let invalid_nonce_message = f"Invalid authwit nonce. When '{from_arg_name}' and 'msg_sender' are the same, '{nonce_arg_name}' must be zero" .as_quoted_str(); diff --git a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr index d207382eac3e..f226b91cb990 100644 --- a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr +++ b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr @@ -5,6 +5,10 @@ pub global STANDARD_AUTH_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_fie 0x1c3bd1fbffc1a52385ed3e4bf6103bd94433b8045777902789da35bee35e4c75, ); +pub global STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = AztecAddress::from_field( + 0x10b200384408725ae02f62ad68f6d43e2e737537487a0b4641cd851368a7cdd7, +); + pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_field( 0x23a16bc3874f7da47c13be4926eebc2a57dbba5d7cf02feb8525ed26dd0844cd, ); diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/authwit.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/authwit.nr index 457b57e727b0..cd2af3ff542f 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/authwit.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/authwit.nr @@ -1,5 +1,5 @@ use crate::{ - authwit::auth::{compute_authwit_message_hash, compute_inner_authwit_hash}, + authwit::utils::{compute_authwit_message_hash, compute_inner_authwit_hash}, context::{PrivateCall, PublicCall}, hash::hash_args, test::helpers::{test_environment::TestEnvironment, txe_oracles}, diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/accounts.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/accounts.nr index be474ae76289..e1654d365d11 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/accounts.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/accounts.nr @@ -1,5 +1,5 @@ use crate::{ - authwit::auth::{compute_authwit_message_hash, IS_VALID_SELECTOR}, + authwit::utils::{compute_authwit_message_hash, IS_VALID_SELECTOR}, context::calls::PrivateStaticCall, keys::getters::get_public_keys, test::helpers::{test_environment::TestEnvironment, txe_oracles}, diff --git a/noir-projects/contract-snapshots/test_programs/compile_failure/authorization_selector_collision/src/main.nr b/noir-projects/contract-snapshots/test_programs/compile_failure/authorization_selector_collision/src/main.nr index 244c23eaea2e..247512324241 100644 --- a/noir-projects/contract-snapshots/test_programs/compile_failure/authorization_selector_collision/src/main.nr +++ b/noir-projects/contract-snapshots/test_programs/compile_failure/authorization_selector_collision/src/main.nr @@ -3,7 +3,7 @@ /// /// Note on the noisy cascade in `expected_error`: the `#[authorization]` macro is /// currently `pub` in aztec-nr but only ever consumed from within aztec-nr itself (in -/// `authwit/auth.nr`). Its generated code references `crate::authwit::*` paths which +/// `authwit/utils.nr`). Its generated code references `crate::authwit::*` paths which /// do not resolve from a downstream crate, producing a handful of cascading resolution /// errors before the collision panic is reached. /// diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 67e0fc346eb5..fbd4fdc941a8 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -28,11 +28,11 @@ members = [ "contracts/fees/sponsored_fpc_contract", "contracts/message_discovery/handshake_registry_contract", "contracts/standard/auth_registry_contract", + "contracts/standard/multi_call_entrypoint_contract", "contracts/standard/public_checks_contract", "contracts/protocol/contract_class_registry_contract", "contracts/protocol/contract_instance_registry_contract", "contracts/protocol/fee_juice_contract", - "contracts/protocol/multi_call_entrypoint_contract", "contracts/protocol_interface/contract_instance_registry_interface", "contracts/protocol_interface/fee_juice_interface", "contracts/test/gas_settings_reader_contract", diff --git a/noir-projects/noir-contracts/bootstrap.sh b/noir-projects/noir-contracts/bootstrap.sh index d5f5ca2cf90d..7c8e941a4314 100755 --- a/noir-projects/noir-contracts/bootstrap.sh +++ b/noir-projects/noir-contracts/bootstrap.sh @@ -144,7 +144,17 @@ function build { if [ "$#" -eq 0 ]; then rm -rf target + mkdir -p target local 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 [ -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/") + fi else local contracts="$@" fi @@ -217,6 +227,22 @@ function format { $NARGO fmt } +# Force-builds standard contracts and tar-balls their artifacts into pinned-standard-contracts.tar.gz. +# Run this at release-branch cut time, then commit the resulting tarball to the release branch. +# Mirrors the v4 `pin-build` mechanism that pins protocol contracts. +function pin-standard-build { + rm -f pinned-standard-contracts.tar.gz + local standard_contracts=$(grep -oP '(?<=contracts/)[^"]+' Nargo.toml | grep "^standard/") + build $standard_contracts || { echo_stderr "Build failed; refusing to create tarball."; return 1; } + local standard_artifacts=$(jq -r '.[]' standard_contracts.json | sed 's/$/.json/') + for a in $standard_artifacts; do + [ -f "target/$a" ] || { echo_stderr "Missing artifact target/$a; refusing to create tarball."; return 1; } + done + echo_stderr "Creating pinned-standard-contracts.tar.gz..." + (cd target && tar czf ../pinned-standard-contracts.tar.gz $standard_artifacts) + echo_stderr "Done. pinned-standard-contracts.tar.gz created. Commit it to pin these artifacts." +} + case "$cmd" in "clean-keys") for artifact in target/*.json; do @@ -231,6 +257,9 @@ case "$cmd" in "compile") VERBOSE=${VERBOSE:-1} build "$@" ;; + "pin-standard-build") + pin-standard-build + ;; *) default_cmd_handler "$@" ;; diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr index b180679358f5..e685055b0b84 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr @@ -9,8 +9,8 @@ pub contract SchnorrAccount { use aztec::{ authwit::{ account::AccountActions, - auth::{compute_authwit_message_hash, compute_authwit_nullifier}, entrypoint::app::AppPayload, + utils::{compute_authwit_message_hash, compute_authwit_nullifier}, }, context::PrivateContext, hash::compute_siloed_nullifier, diff --git a/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr index 0544820909ac..1353b6513691 100644 --- a/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr @@ -8,7 +8,7 @@ use aztec::macros::aztec; #[aztec] pub contract SimulatedEcdsaAccount { use aztec::{ - authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload}, + authwit::{account::AccountActions, entrypoint::app::AppPayload, utils::IS_VALID_SELECTOR}, context::PrivateContext, macros::{functions::{allow_phase_change, external, view}, storage::storage}, messages::encoding::MESSAGE_CIPHERTEXT_LEN, diff --git a/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr index 9d3959ed947f..2b5d318f35d6 100644 --- a/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr @@ -10,7 +10,7 @@ use aztec::macros::aztec; #[aztec] pub contract SimulatedSchnorrAccount { use aztec::{ - authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload}, + authwit::{account::AccountActions, entrypoint::app::AppPayload, utils::IS_VALID_SELECTOR}, context::PrivateContext, macros::{functions::{allow_phase_change, external, view}, storage::storage}, messages::encoding::MESSAGE_CIPHERTEXT_LEN, diff --git a/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr index c46217c08951..3c59840e99dc 100644 --- a/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/app_subscription_contract/src/main.nr @@ -44,7 +44,7 @@ pub contract AppSubscription { use crate::{config::Config, dapp_payload::DAppPayload, subscription_note::SubscriptionNote}; use aztec::{ - authwit::auth::assert_current_call_valid_authwit, + authwit::private::assert_current_call_valid_authwit, macros::{functions::{allow_phase_change, external, initializer}, storage::storage}, messages::message_delivery::MessageDelivery, oracle::notes::set_sender_for_tags, @@ -78,7 +78,7 @@ pub contract AppSubscription { unsafe { set_sender_for_tags(user_address) }; // This function takes a generic argument that corresponds to the number of params - // the parent takes. See aztec-nr/src/authwit/auth.nr for more details. + // the parent takes. See aztec-nr/src/authwit/private.nr for more details. assert_current_call_valid_authwit::<2>(self.context, user_address); let user_expiry_block_number = &mut 0; diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr index f4980f753298..fceceab82494 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr @@ -9,7 +9,7 @@ use aztec::macros::aztec; pub contract NFT { use crate::types::nft_note::{NFTNote, PartialNFTNote}; use aztec::{ - authwit::auth::compute_authwit_nullifier, + authwit::utils::compute_authwit_nullifier, macros::{functions::{authorize_once, external, initializer, internal, only_self, view}, storage::storage}, messages::message_delivery::MessageDelivery, note::{ diff --git a/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr index eb2f64c7cc72..629eb58ca441 100644 --- a/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr @@ -11,7 +11,7 @@ pub contract SimpleToken { use compressed_string::FieldCompressedString; use aztec::{ - authwit::auth::compute_authwit_nullifier, + authwit::utils::compute_authwit_nullifier, context::{PrivateCall, PrivateContext}, macros::{ events::event, diff --git a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr index bfced8c7bb61..16ce8026c518 100644 --- a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr @@ -16,7 +16,7 @@ pub contract Token { use compressed_string::FieldCompressedString; use aztec::{ - authwit::auth::compute_authwit_nullifier, + authwit::utils::compute_authwit_nullifier, context::{PrivateCall, PrivateContext}, macros::{ events::event, diff --git a/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr index 9ff9dbb511da..8471a5d90eaf 100644 --- a/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr @@ -8,8 +8,9 @@ use aztec::macros::aztec; #[aztec] pub contract Uniswap { use aztec::{ - authwit::auth::{ - assert_current_call_valid_authwit_public, compute_authwit_message_hash_from_call, set_authorized, + authwit::{ + public::{assert_current_call_valid_authwit_public, set_authorized}, + utils::compute_authwit_message_hash_from_call, }, macros::{functions::{external, initializer, only_self}, storage::storage}, protocol::{abis::function_selector::FunctionSelector, address::{AztecAddress, EthAddress}, traits::ToField}, diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/authwit/auth.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/authwit/auth.nr index ffce65a9229c..38e8e172e189 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/authwit/auth.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/authwit/auth.nr @@ -1,4 +1,4 @@ -use crate::context::{gas::GasOpts, PrivateContext, PublicContext}; +use crate::context::PrivateContext; use crate::protocol::{ abis::function_selector::FunctionSelector, address::AztecAddress, @@ -6,7 +6,6 @@ use crate::protocol::{ hash::poseidon2_hash_with_separator, traits::ToField, }; -use crate::standard_addresses::STANDARD_AUTH_REGISTRY_ADDRESS; /// Success indicator for authwit - 4 last bytes of poseidon2_hash_bytes("IS_VALID()") pub global IS_VALID_SELECTOR: Field = 0x47dacd73; @@ -52,39 +51,6 @@ pub fn assert_inner_hash_valid_authwit( context.push_nullifier(nullifier); } -/// Assert that `on_behalf_of` has authorized the current call in the authentication registry -/// -/// @param on_behalf_of The address that has allegedly authorized the current call -pub unconstrained fn assert_current_call_valid_authwit_public( - context: PublicContext, - on_behalf_of: AztecAddress, -) { - let inner_hash = compute_inner_authwit_hash([ - context.maybe_msg_sender().unwrap().to_field(), - context.selector().to_field(), - context.get_args_hash(), - ]); - assert_inner_hash_valid_authwit_public(context, on_behalf_of, inner_hash); -} - -/// Assert that `on_behalf_of` has authorized a specific `inner_hash` in the authentication registry -/// -/// @param on_behalf_of The address that has allegedly authorized the `inner_hash` -pub unconstrained fn assert_inner_hash_valid_authwit_public( - context: PublicContext, - on_behalf_of: AztecAddress, - inner_hash: Field, -) { - let results: [Field] = context.call_public_function( - STANDARD_AUTH_REGISTRY_ADDRESS, - comptime { FunctionSelector::from_signature("consume((Field),Field)") }, - [on_behalf_of.to_field(), inner_hash], - GasOpts::default(), - ); - assert(results.len() == 1, "Invalid response from registry"); - assert(results[0] == IS_VALID_SELECTOR, "Message not authorized by account"); -} - /// Computes the `inner_hash` of the authentication witness /// /// @param args The arguments to hash @@ -120,30 +86,3 @@ pub fn compute_authwit_message_hash( DOM_SEP__AUTHWIT_OUTER, ) } - -/// Helper function to set the authorization status of a message hash -/// -/// @param message_hash The hash of the message to authorize -/// @param authorize True if the message should be authorized, false if it should be revoked -pub unconstrained fn set_authorized(context: PublicContext, message_hash: Field, authorize: bool) { - let res = context.call_public_function( - STANDARD_AUTH_REGISTRY_ADDRESS, - comptime { FunctionSelector::from_signature("set_authorized(Field,bool)") }, - [message_hash, authorize as Field], - GasOpts::default(), - ); - assert(res.len() == 0); -} - -/// Helper function to reject all authwits -/// -/// @param reject True if all authwits should be rejected, false otherwise -pub unconstrained fn set_reject_all(context: PublicContext, reject: bool) { - let res = context.call_public_function( - STANDARD_AUTH_REGISTRY_ADDRESS, - comptime { FunctionSelector::from_signature("set_reject_all(bool)") }, - [reject as Field], - GasOpts::default(), - ); - assert(res.len() == 0); -} diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr index d207382eac3e..f226b91cb990 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr @@ -5,6 +5,10 @@ pub global STANDARD_AUTH_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_fie 0x1c3bd1fbffc1a52385ed3e4bf6103bd94433b8045777902789da35bee35e4c75, ); +pub global STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = AztecAddress::from_field( + 0x10b200384408725ae02f62ad68f6d43e2e737537487a0b4641cd851368a7cdd7, +); + pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_field( 0x23a16bc3874f7da47c13be4926eebc2a57dbba5d7cf02feb8525ed26dd0844cd, ); diff --git a/noir-projects/noir-contracts/contracts/standard/auth_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/standard/auth_registry_contract/src/main.nr index 9b7e9f7ffad8..454b7d91c1eb 100644 --- a/noir-projects/noir-contracts/contracts/standard/auth_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/standard/auth_registry_contract/src/main.nr @@ -17,7 +17,10 @@ use aztec::macros::aztec; #[aztec] pub contract AuthRegistry { use aztec::{ - authwit::auth::{assert_current_call_valid_authwit, compute_authwit_message_hash, IS_VALID_SELECTOR}, + authwit::{ + private::assert_current_call_valid_authwit, + utils::{compute_authwit_message_hash, IS_VALID_SELECTOR}, + }, macros::{functions::{external, only_self, view}, storage::storage}, protocol::address::AztecAddress, state_vars::{Map, PublicMutable}, diff --git a/noir-projects/noir-contracts/contracts/protocol/multi_call_entrypoint_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/standard/multi_call_entrypoint_contract/Nargo.toml similarity index 100% rename from noir-projects/noir-contracts/contracts/protocol/multi_call_entrypoint_contract/Nargo.toml rename to noir-projects/noir-contracts/contracts/standard/multi_call_entrypoint_contract/Nargo.toml diff --git a/noir-projects/noir-contracts/contracts/protocol/multi_call_entrypoint_contract/src/main.nr b/noir-projects/noir-contracts/contracts/standard/multi_call_entrypoint_contract/src/main.nr similarity index 100% rename from noir-projects/noir-contracts/contracts/protocol/multi_call_entrypoint_contract/src/main.nr rename to noir-projects/noir-contracts/contracts/standard/multi_call_entrypoint_contract/src/main.nr diff --git a/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr index 96a12efde57b..851e12d560ed 100644 --- a/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/auth_wit_test_contract/src/main.nr @@ -3,7 +3,7 @@ use aztec::macros::aztec; #[aztec] pub contract AuthWitTest { use aztec::{ - authwit::auth::{assert_inner_hash_valid_authwit, assert_inner_hash_valid_authwit_public}, + authwit::{private::assert_inner_hash_valid_authwit, public::assert_inner_hash_valid_authwit_public}, macros::functions::{authorize_once, external}, protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}, }; diff --git a/noir-projects/noir-contracts/protocol_contracts.json b/noir-projects/noir-contracts/protocol_contracts.json index f7a94fde113f..108e8ed94cbe 100644 --- a/noir-projects/noir-contracts/protocol_contracts.json +++ b/noir-projects/noir-contracts/protocol_contracts.json @@ -1,6 +1,5 @@ [ "contract_instance_registry_contract-ContractInstanceRegistry", "contract_class_registry_contract-ContractClassRegistry", - "multi_call_entrypoint_contract-MultiCallEntrypoint", "fee_juice_contract-FeeJuice" ] diff --git a/noir-projects/noir-contracts/standard_contracts.json b/noir-projects/noir-contracts/standard_contracts.json new file mode 100644 index 000000000000..88511135ff05 --- /dev/null +++ b/noir-projects/noir-contracts/standard_contracts.json @@ -0,0 +1,5 @@ +[ + "auth_registry_contract-AuthRegistry", + "public_checks_contract-PublicChecks", + "multi_call_entrypoint_contract-MultiCallEntrypoint" +] diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 3d487b283ed1..7d4f46a3ac66 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -228,10 +228,9 @@ pub global CONTRACT_INSTANCE_UPDATED_MAGIC_VALUE: Field = // Chosen to be a multiple of 3 (-1 to account for the separator), to optimize the poseidon2 hash. pub global MAX_PROTOCOL_CONTRACTS: u32 = 11; // Address 0 is not a protocol contract. +pub global CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS: AztecAddress = AztecAddress::from_field(1); pub global CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS: AztecAddress = AztecAddress::from_field(2); -pub global CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS: AztecAddress = AztecAddress::from_field(3); -pub global MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = AztecAddress::from_field(4); -pub global FEE_JUICE_ADDRESS: AztecAddress = AztecAddress::from_field(5); +pub global FEE_JUICE_ADDRESS: AztecAddress = AztecAddress::from_field(3); // `SIDE_EFFECT_MASKING_ADDRESS` is used by the protocol circuits to silo the padding side effects. It's not a protocol // contract (hence its address is greater than `MAX_PROTOCOL_CONTRACTS`). diff --git a/yarn-project/aztec-node/package.json b/yarn-project/aztec-node/package.json index ef0347475f3d..8310aca29741 100644 --- a/yarn-project/aztec-node/package.json +++ b/yarn-project/aztec-node/package.json @@ -85,6 +85,7 @@ "@aztec/sequencer-client": "workspace:^", "@aztec/simulator": "workspace:^", "@aztec/slasher": "workspace:^", + "@aztec/standard-contracts": "workspace:^", "@aztec/stdlib": "workspace:^", "@aztec/telemetry-client": "workspace:^", "@aztec/validator-client": "workspace:^", diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index edba020f1e27..39d4dba02109 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -52,6 +52,7 @@ import { type Watcher, createSlasher, } from '@aztec/slasher'; +import { STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS } from '@aztec/standard-contracts/multi-call-entrypoint'; import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { @@ -1734,7 +1735,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb classRegistry: ProtocolContractAddress.ContractClassRegistry, feeJuice: ProtocolContractAddress.FeeJuice, instanceRegistry: ProtocolContractAddress.ContractInstanceRegistry, - multiCallEntrypoint: ProtocolContractAddress.MultiCallEntrypoint, + multiCallEntrypoint: STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS, }); } diff --git a/yarn-project/aztec-node/tsconfig.json b/yarn-project/aztec-node/tsconfig.json index 86bc75e353ac..c6c43e7a4ac0 100644 --- a/yarn-project/aztec-node/tsconfig.json +++ b/yarn-project/aztec-node/tsconfig.json @@ -66,6 +66,9 @@ { "path": "../slasher" }, + { + "path": "../standard-contracts" + }, { "path": "../stdlib" }, diff --git a/yarn-project/aztec.js/src/api/protocol.ts b/yarn-project/aztec.js/src/api/protocol.ts index ad56540ee4e6..595de6c6d507 100644 --- a/yarn-project/aztec.js/src/api/protocol.ts +++ b/yarn-project/aztec.js/src/api/protocol.ts @@ -4,4 +4,3 @@ export { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; export { ContractClassRegistryContract } from '../contract/protocol_contracts/contract-class-registry.js'; export { ContractInstanceRegistryContract } from '../contract/protocol_contracts/contract-instance-registry.js'; export { FeeJuiceContract } from '../contract/protocol_contracts/fee-juice.js'; -export { MultiCallEntrypointContract } from '../contract/protocol_contracts/multi-call-entrypoint.js'; diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index eabc7415325e..5e7ad6711db1 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -117,10 +117,9 @@ export const CONTRACT_CLASS_REGISTRY_UTILITY_FUNCTION_BROADCASTED_MAGIC_VALUE = export const CONTRACT_INSTANCE_PUBLISHED_MAGIC_VALUE = 10538216027419913765597387738085647348651103543680388181336823392401502757423n; export const CONTRACT_INSTANCE_UPDATED_MAGIC_VALUE = 20721543224513346060908370400407150739273836456436647488068002302723900469047n; export const MAX_PROTOCOL_CONTRACTS = 11; +export const CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS = 1; export const CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS = 2; -export const CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS = 3; -export const MULTI_CALL_ENTRYPOINT_ADDRESS = 4; -export const FEE_JUICE_ADDRESS = 5; +export const FEE_JUICE_ADDRESS = 3; export const SIDE_EFFECT_MASKING_ADDRESS = 19523154334483583633304358390644137470227519736821975910774528428729027989987n; export const NULL_MSG_SENDER_CONTRACT_ADDRESS = 21888242871839275222246405745257275088548364400416034343698204186575808495616n; export const CONTRACT_CLASS_REGISTRY_BYTECODE_CAPSULE_SLOT = 14193106819744442689484501689686180698286338820089744102289275815863062044599n; diff --git a/yarn-project/constants/src/scripts/constants.in.ts b/yarn-project/constants/src/scripts/constants.in.ts index 9f6f8863c436..8f8d64a305d5 100644 --- a/yarn-project/constants/src/scripts/constants.in.ts +++ b/yarn-project/constants/src/scripts/constants.in.ts @@ -25,7 +25,6 @@ const CPP_CONSTANTS = [ 'MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS', 'CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS', 'CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS', - 'MULTI_CALL_ENTRYPOINT_ADDRESS', 'FEE_JUICE_ADDRESS', 'TX_DA_GAS_OVERHEAD', 'FEE_JUICE_BALANCES_SLOT', @@ -173,7 +172,6 @@ const PIL_CONSTANTS = [ 'UPDATES_DELAYED_PUBLIC_MUTABLE_VALUES_LEN', 'CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS', 'CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS', - 'MULTI_CALL_ENTRYPOINT_ADDRESS', 'FEE_JUICE_ADDRESS', 'FEE_JUICE_BALANCES_SLOT', 'TIMESTAMP_OF_CHANGE_BIT_SIZE', diff --git a/yarn-project/entrypoints/package.json b/yarn-project/entrypoints/package.json index f2254b0e34cc..6b271d602ccc 100644 --- a/yarn-project/entrypoints/package.json +++ b/yarn-project/entrypoints/package.json @@ -70,6 +70,7 @@ "@aztec/constants": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/protocol-contracts": "workspace:^", + "@aztec/standard-contracts": "workspace:^", "@aztec/stdlib": "workspace:^", "tslib": "^2.4.0", "zod": "^4" diff --git a/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts b/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts index 9eef30e7565b..512079249280 100644 --- a/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts +++ b/yarn-project/entrypoints/src/default_multi_call_entrypoint.ts @@ -1,5 +1,5 @@ import { Fr } from '@aztec/foundation/curves/bn254'; -import { ProtocolContractAddress } from '@aztec/protocol-contracts'; +import { STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS } from '@aztec/standard-contracts/multi-call-entrypoint/constants'; import { type FunctionAbi, FunctionCall, FunctionSelector, encodeArguments } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { GasSettings } from '@aztec/stdlib/gas'; @@ -12,7 +12,7 @@ import type { ChainInfo, EntrypointInterface } from './interfaces.js'; * Implementation for an entrypoint interface that can execute multiple function calls in a single transaction */ export class DefaultMultiCallEntrypoint implements EntrypointInterface { - constructor(private address: AztecAddress = ProtocolContractAddress.MultiCallEntrypoint) {} + constructor(private address: AztecAddress = STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS) {} async createTxExecutionRequest( exec: ExecutionPayload, diff --git a/yarn-project/entrypoints/tsconfig.json b/yarn-project/entrypoints/tsconfig.json index d731cf7a6a1c..2d995153519d 100644 --- a/yarn-project/entrypoints/tsconfig.json +++ b/yarn-project/entrypoints/tsconfig.json @@ -15,6 +15,9 @@ { "path": "../protocol-contracts" }, + { + "path": "../standard-contracts" + }, { "path": "../stdlib" } diff --git a/yarn-project/protocol-contracts/src/multi-call-entrypoint/index.ts b/yarn-project/protocol-contracts/src/multi-call-entrypoint/index.ts deleted file mode 100644 index e7dae3778f4c..000000000000 --- a/yarn-project/protocol-contracts/src/multi-call-entrypoint/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { loadContractArtifact } from '@aztec/stdlib/abi'; -import type { NoirCompiledContract } from '@aztec/stdlib/noir'; - -import MultiCallEntrypointJson from '../../artifacts/MultiCallEntrypoint.json' with { type: 'json' }; -import { makeProtocolContract } from '../make_protocol_contract.js'; -import type { ProtocolContract } from '../protocol_contract.js'; - -export const MultiCallEntrypointArtifact = loadContractArtifact(MultiCallEntrypointJson as NoirCompiledContract); - -let protocolContract: ProtocolContract; - -/** Returns the canonical deployment of the contract. */ -export function getCanonicalMultiCallEntrypoint(): Promise { - if (!protocolContract) { - protocolContract = makeProtocolContract('MultiCallEntrypoint', MultiCallEntrypointArtifact); - } - return Promise.resolve(protocolContract); -} diff --git a/yarn-project/protocol-contracts/src/multi-call-entrypoint/lazy.ts b/yarn-project/protocol-contracts/src/multi-call-entrypoint/lazy.ts deleted file mode 100644 index d1fe22edfcce..000000000000 --- a/yarn-project/protocol-contracts/src/multi-call-entrypoint/lazy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type ContractArtifact, loadContractArtifact } from '@aztec/stdlib/abi'; - -import { makeProtocolContract } from '../make_protocol_contract.js'; -import type { ProtocolContract } from '../protocol_contract.js'; - -let protocolContract: ProtocolContract; -let protocolContractArtifact: ContractArtifact; - -export async function getMultiCallEntrypointArtifact(): Promise { - if (!protocolContractArtifact) { - // Cannot assert this import as it's incompatible with bundlers like vite - // https://github.com/vitejs/vite/issues/19095#issuecomment-2566074352 - // Even if now supported by al major browsers, the MIME type is replaced with - // "text/javascript" - // In the meantime, this lazy import is INCOMPATIBLE WITH NODEJS - const { default: multiCallEntrypointJson } = await import('../../artifacts/MultiCallEntrypoint.json'); - protocolContractArtifact = loadContractArtifact(multiCallEntrypointJson); - } - return protocolContractArtifact; -} - -/** Returns the canonical deployment of the auth registry. */ -export async function getCanonicalMultiCallEntrypoint(): Promise { - if (!protocolContract) { - const multiCallEntrypointArtifact = await getMultiCallEntrypointArtifact(); - protocolContract = makeProtocolContract('MultiCallEntrypoint', multiCallEntrypointArtifact); - } - return protocolContract; -} diff --git a/yarn-project/protocol-contracts/src/provider/bundle.ts b/yarn-project/protocol-contracts/src/provider/bundle.ts index c1950786567a..4a036f96874b 100644 --- a/yarn-project/protocol-contracts/src/provider/bundle.ts +++ b/yarn-project/protocol-contracts/src/provider/bundle.ts @@ -4,7 +4,6 @@ import { ContractClassRegistryArtifact } from '../class-registry/index.js'; import { FeeJuiceArtifact } from '../fee-juice/index.js'; import { ContractInstanceRegistryArtifact } from '../instance-registry/index.js'; import { makeProtocolContract } from '../make_protocol_contract.js'; -import { MultiCallEntrypointArtifact } from '../multi-call-entrypoint/index.js'; import type { ProtocolContract } from '../protocol_contract.js'; import type { ProtocolContractName } from '../protocol_contract_data.js'; import type { ProtocolContractsProvider } from './protocol_contracts_provider.js'; @@ -12,7 +11,6 @@ import type { ProtocolContractsProvider } from './protocol_contracts_provider.js export const ProtocolContractArtifact: Record = { ContractInstanceRegistry: ContractInstanceRegistryArtifact, ContractClassRegistry: ContractClassRegistryArtifact, - MultiCallEntrypoint: MultiCallEntrypointArtifact, FeeJuice: FeeJuiceArtifact, }; diff --git a/yarn-project/protocol-contracts/src/provider/lazy.ts b/yarn-project/protocol-contracts/src/provider/lazy.ts index 1e2f7b6cc345..858b9742c337 100644 --- a/yarn-project/protocol-contracts/src/provider/lazy.ts +++ b/yarn-project/protocol-contracts/src/provider/lazy.ts @@ -1,7 +1,6 @@ import { getCanonicalClassRegistry } from '../class-registry/lazy.js'; import { getCanonicalFeeJuice } from '../fee-juice/lazy.js'; import { getCanonicalInstanceRegistry } from '../instance-registry/lazy.js'; -import { getCanonicalMultiCallEntrypoint } from '../multi-call-entrypoint/lazy.js'; import type { ProtocolContract } from '../protocol_contract.js'; import type { ProtocolContractName } from '../protocol_contract_data.js'; import type { ProtocolContractsProvider } from './protocol_contracts_provider.js'; @@ -13,8 +12,6 @@ export class LazyProtocolContractsProvider implements ProtocolContractsProvider return getCanonicalInstanceRegistry(); case 'ContractClassRegistry': return getCanonicalClassRegistry(); - case 'MultiCallEntrypoint': - return getCanonicalMultiCallEntrypoint(); case 'FeeJuice': return getCanonicalFeeJuice(); default: diff --git a/yarn-project/protocol-contracts/src/scripts/generate_data.ts b/yarn-project/protocol-contracts/src/scripts/generate_data.ts index 5f2ae3407940..e97b2909b948 100644 --- a/yarn-project/protocol-contracts/src/scripts/generate_data.ts +++ b/yarn-project/protocol-contracts/src/scripts/generate_data.ts @@ -8,7 +8,6 @@ import { CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, FEE_JUICE_ADDRESS, MAX_PROTOCOL_CONTRACTS, - MULTI_CALL_ENTRYPOINT_ADDRESS, } from '@aztec/constants'; import { makeTuple } from '@aztec/foundation/array'; import { Fr } from '@aztec/foundation/curves/bn254'; @@ -40,7 +39,6 @@ const salt = new Fr(1); const contractAddressMapping: { [name: string]: number } = { ContractInstanceRegistry: CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, ContractClassRegistry: CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS, - MultiCallEntrypoint: MULTI_CALL_ENTRYPOINT_ADDRESS, FeeJuice: FEE_JUICE_ADDRESS, }; diff --git a/yarn-project/pxe/package.json b/yarn-project/pxe/package.json index 35908adc42f2..1aa1045bd898 100644 --- a/yarn-project/pxe/package.json +++ b/yarn-project/pxe/package.json @@ -83,6 +83,7 @@ "@aztec/noir-types": "workspace:*", "@aztec/protocol-contracts": "workspace:^", "@aztec/simulator": "workspace:^", + "@aztec/standard-contracts": "workspace:^", "@aztec/stdlib": "workspace:^", "koa": "^2.16.1", "koa-router": "^13.1.1", diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts index a09cae2c6e12..43a954f4f0db 100644 --- a/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts +++ b/yarn-project/pxe/src/entrypoints/client/bundle/utils.ts @@ -3,6 +3,7 @@ import { createLogger } from '@aztec/foundation/log'; import { createStore } from '@aztec/kv-store/indexeddb'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { WASMSimulator } from '@aztec/simulator/client'; +import { getStandardMultiCallEntrypoint } from '@aztec/standard-contracts/multi-call-entrypoint'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PXEConfig } from '../../../config/index.js'; @@ -49,6 +50,7 @@ export async function createPXE( prover = new BBBundlePrivateKernelProver(simulator, { ...options.proverOrOptions, logger: proverLogger }); } const protocolContractsProvider = new BundledProtocolContractsProvider(); + const multiCallEntrypointProvider = { getStandardMultiCallEntrypoint }; const pxeLogger = loggers.pxe ?? createLogger('pxe:service', { actor }); const pxe = await PXE.create({ @@ -57,6 +59,7 @@ export async function createPXE( proofCreator: prover, simulator, protocolContractsProvider, + multiCallEntrypointProvider, config, loggerOrSuffix: pxeLogger, hooks: options.hooks, diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts index 821a4ae781b9..5027528ea86a 100644 --- a/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts +++ b/yarn-project/pxe/src/entrypoints/client/lazy/utils.ts @@ -3,6 +3,7 @@ import { createLogger } from '@aztec/foundation/log'; import { createStore } from '@aztec/kv-store/indexeddb'; import { LazyProtocolContractsProvider } from '@aztec/protocol-contracts/providers/lazy'; import { WASMSimulator } from '@aztec/simulator/client'; +import { getStandardMultiCallEntrypoint } from '@aztec/standard-contracts/multi-call-entrypoint/lazy'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PXEConfig } from '../../../config/index.js'; @@ -49,6 +50,7 @@ export async function createPXE( prover = new BBLazyPrivateKernelProver(simulator, { ...options.proverOrOptions, logger: proverLogger }); } const protocolContractsProvider = new LazyProtocolContractsProvider(); + const multiCallEntrypointProvider = { getStandardMultiCallEntrypoint }; const pxeLogger = loggers.pxe ?? createLogger('pxe:service', { actor }); const pxe = await PXE.create({ @@ -57,6 +59,7 @@ export async function createPXE( proofCreator: prover, simulator, protocolContractsProvider, + multiCallEntrypointProvider, config, loggerOrSuffix: pxeLogger, hooks: options.hooks, diff --git a/yarn-project/pxe/src/entrypoints/server/utils.ts b/yarn-project/pxe/src/entrypoints/server/utils.ts index 4521175610e5..755be094d616 100644 --- a/yarn-project/pxe/src/entrypoints/server/utils.ts +++ b/yarn-project/pxe/src/entrypoints/server/utils.ts @@ -5,6 +5,7 @@ import { createStore } from '@aztec/kv-store/lmdb-v2'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { MemoryCircuitRecorder, SimulatorRecorderWrapper, WASMSimulator } from '@aztec/simulator/client'; import { FileCircuitRecorder } from '@aztec/simulator/testing'; +import { getStandardMultiCallEntrypoint } from '@aztec/standard-contracts/multi-call-entrypoint'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { PXEConfig } from '../../config/index.js'; @@ -59,6 +60,7 @@ export async function createPXE( } const protocolContractsProvider = new BundledProtocolContractsProvider(); + const multiCallEntrypointProvider = { getStandardMultiCallEntrypoint }; const pxeLogger = loggers.pxe ?? createLogger('pxe:service', { actor }); const pxe = await PXE.create({ @@ -67,6 +69,7 @@ export async function createPXE( proofCreator: prover, simulator, protocolContractsProvider, + multiCallEntrypointProvider, config: configWithContracts, loggerOrSuffix: pxeLogger, hooks: options.hooks, diff --git a/yarn-project/pxe/src/pxe.test.ts b/yarn-project/pxe/src/pxe.test.ts index a88900347f8a..323dd8adef8b 100644 --- a/yarn-project/pxe/src/pxe.test.ts +++ b/yarn-project/pxe/src/pxe.test.ts @@ -7,6 +7,10 @@ import { AztecLMDBStoreV2, openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { TestContractArtifact } from '@aztec/noir-test-contracts.js/Test'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { WASMSimulator } from '@aztec/simulator/client'; +import { + STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS, + getStandardMultiCallEntrypoint, +} from '@aztec/standard-contracts/multi-call-entrypoint'; import { EventSelector, FunctionType } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { @@ -45,6 +49,7 @@ describe('PXE', () => { const simulator = new WASMSimulator(); const kernelProver = new BBBundlePrivateKernelProver(simulator); const protocolContractsProvider = new BundledProtocolContractsProvider(); + const multiCallEntrypointProvider = { getStandardMultiCallEntrypoint }; const config: PXEConfig = { ...emptyChainConfig, l2BlockBatchSize: 50, @@ -91,6 +96,7 @@ describe('PXE', () => { proofCreator: kernelProver, simulator, protocolContractsProvider, + multiCallEntrypointProvider, config, }); }, 120_000); @@ -137,6 +143,15 @@ describe('PXE', () => { expect(contractAddresses).toEqual(expect.arrayContaining(expectedContractAddresses)); }); + it('preloads the standard multi-call entrypoint on creation', async () => { + const { instance: expectedInstance, artifact: expectedArtifact } = await getStandardMultiCallEntrypoint(); + const instance = await pxe.getContractInstance(STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS); + expect(instance).toEqual(expectedInstance); + + const artifact = await pxe.getContractArtifact(expectedInstance.currentContractClassId); + expect(artifact).toEqual(expectedArtifact); + }); + it('registers a class and adds a contract for it', async () => { const artifact = randomContractArtifact(); const contractClass = await getContractClassFromArtifact(artifact); diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 0f68dfe529f0..6fd72c3c24f1 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -141,6 +141,20 @@ export type ExecuteUtilityOpts = { scopes: AztecAddress[]; }; +/** + * Supplies the standard multi-call entrypoint that every PXE must know about regardless of which + * wallet drives it: the SDK's self-paid account deploy flow ({@link DeployAccountMethod} with + * `from = NO_FROM`) routes its payload through it, so a PXE that did not register it would fail + * contract sync with an opaque "no contract instance" error. + * + * Injected the same way as {@link ProtocolContractsProvider} so the PXE never statically imports the + * bundled artifacts, keeping the bundle/lazy split intact. + */ +export type MultiCallEntrypointProvider = { + /** Returns the canonical multi-call entrypoint instance and artifact to preload. */ + getStandardMultiCallEntrypoint: () => Promise<{ instance: ContractInstanceWithAddress; artifact: ContractArtifact }>; +}; + /** Args for PXE.create. */ export type PXECreateArgs = { /** The Aztec node to connect to. */ @@ -153,6 +167,8 @@ export type PXECreateArgs = { simulator: CircuitSimulator; /** Provider for protocol contract artifacts and instances. */ protocolContractsProvider: ProtocolContractsProvider; + /** Provider for the standard multi-call entrypoint the PXE preloads. */ + multiCallEntrypointProvider: MultiCallEntrypointProvider; /** PXE configuration options. */ config: PXEConfig; /** Optional logger instance or string suffix for the logger name. */ @@ -188,6 +204,7 @@ export class PXE { private autoSync: boolean, private proofCreator: PrivateKernelProver, private protocolContractsProvider: ProtocolContractsProvider, + private multiCallEntrypointProvider: MultiCallEntrypointProvider, private log: Logger, private jobQueue: SerialQueue, private jobCoordinator: JobCoordinator, @@ -208,6 +225,7 @@ export class PXE { proofCreator, simulator, protocolContractsProvider, + multiCallEntrypointProvider, config, loggerOrSuffix, hooks, @@ -301,6 +319,7 @@ export class PXE { config.autoSync, proofCreator, protocolContractsProvider, + multiCallEntrypointProvider, log, jobQueue, jobCoordinator, @@ -317,6 +336,7 @@ export class PXE { pxe.jobQueue.start(); await pxe.#registerProtocolContracts(); + await pxe.#registerMultiCallEntrypoint(); log.info(`Started PXE connected to chain ${info.l1ChainId} version ${info.rollupVersion}`); return pxe; } @@ -402,6 +422,14 @@ export class PXE { this.log.verbose(`Registered protocol contracts in pxe`, registered); } + async #registerMultiCallEntrypoint() { + const { instance, artifact } = await this.multiCallEntrypointProvider.getStandardMultiCallEntrypoint(); + await this.registerContract({ instance, artifact }); + this.log.verbose(`Registered standard multi-call entrypoint in pxe`, { + MultiCallEntrypoint: instance.address.toString(), + }); + } + // Executes the entrypoint private function, as well as all nested private // functions that might arise. async #executePrivate({ diff --git a/yarn-project/pxe/tsconfig.json b/yarn-project/pxe/tsconfig.json index 539e38403082..8db630863388 100644 --- a/yarn-project/pxe/tsconfig.json +++ b/yarn-project/pxe/tsconfig.json @@ -36,6 +36,9 @@ { "path": "../simulator" }, + { + "path": "../standard-contracts" + }, { "path": "../stdlib" }, diff --git a/yarn-project/standard-contracts/package.json b/yarn-project/standard-contracts/package.json index 668618895719..a2139adf2879 100644 --- a/yarn-project/standard-contracts/package.json +++ b/yarn-project/standard-contracts/package.json @@ -9,7 +9,8 @@ "./*": "./dest/*/index.js", "./*/lazy": "./dest/*/lazy.js", "./auth-registry/constants": "./dest/auth-registry/constants.js", - "./public-checks/constants": "./dest/public-checks/constants.js" + "./public-checks/constants": "./dest/public-checks/constants.js", + "./multi-call-entrypoint/constants": "./dest/multi-call-entrypoint/constants.js" }, "typedocOptions": { "entryPoints": [ diff --git a/yarn-project/standard-contracts/src/contract_data.ts b/yarn-project/standard-contracts/src/contract_data.ts index b9cd189869a6..228538632d3a 100644 --- a/yarn-project/standard-contracts/src/contract_data.ts +++ b/yarn-project/standard-contracts/src/contract_data.ts @@ -59,6 +59,11 @@ export const STANDARD_CONTRACT_DEPLOYER = AztecAddress.zero(); */ export const standardContracts: { name: string; src: string; nrConst: string | null }[] = [ { name: 'AuthRegistry', src: 'auth_registry_contract-AuthRegistry', nrConst: 'STANDARD_AUTH_REGISTRY_ADDRESS' }, + { + name: 'MultiCallEntrypoint', + src: 'multi_call_entrypoint_contract-MultiCallEntrypoint', + nrConst: 'STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS', + }, { name: 'PublicChecks', src: 'public_checks_contract-PublicChecks', nrConst: 'STANDARD_PUBLIC_CHECKS_ADDRESS' }, ]; diff --git a/yarn-project/standard-contracts/src/index.ts b/yarn-project/standard-contracts/src/index.ts index 6dd99a458ca7..c8e94295e162 100644 --- a/yarn-project/standard-contracts/src/index.ts +++ b/yarn-project/standard-contracts/src/index.ts @@ -1,2 +1,3 @@ export * from './auth-registry/index.js'; +export * from './multi-call-entrypoint/index.js'; export * from './public-checks/index.js'; diff --git a/yarn-project/standard-contracts/src/multi-call-entrypoint/constants.ts b/yarn-project/standard-contracts/src/multi-call-entrypoint/constants.ts new file mode 100644 index 000000000000..f0a134722913 --- /dev/null +++ b/yarn-project/standard-contracts/src/multi-call-entrypoint/constants.ts @@ -0,0 +1,8 @@ +// Lightweight metadata leaf export for browser bundles: importing from +// `@aztec/standard-contracts/multi-call-entrypoint/constants` avoids dragging in the +// `MultiCallEntrypoint.json` static import. +import { StandardContractAddress, StandardContractClassId, StandardContractSalt } from '../standard_contract_data.js'; + +export const STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS = StandardContractAddress.MultiCallEntrypoint; +export const STANDARD_MULTI_CALL_ENTRYPOINT_CLASS_ID = StandardContractClassId.MultiCallEntrypoint; +export const STANDARD_MULTI_CALL_ENTRYPOINT_SALT = StandardContractSalt.MultiCallEntrypoint; diff --git a/yarn-project/standard-contracts/src/multi-call-entrypoint/index.ts b/yarn-project/standard-contracts/src/multi-call-entrypoint/index.ts new file mode 100644 index 000000000000..b9fa55b0b0a7 --- /dev/null +++ b/yarn-project/standard-contracts/src/multi-call-entrypoint/index.ts @@ -0,0 +1,24 @@ +import { loadContractArtifact } from '@aztec/stdlib/abi'; +import type { NoirCompiledContract } from '@aztec/stdlib/noir'; + +import MultiCallEntrypointJson from '../../artifacts/MultiCallEntrypoint.json' with { type: 'json' }; +import { makeStandardContract } from '../make_standard_contract.js'; +import type { StandardContract } from '../standard_contract.js'; + +export { + STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS, + STANDARD_MULTI_CALL_ENTRYPOINT_CLASS_ID, + STANDARD_MULTI_CALL_ENTRYPOINT_SALT, +} from './constants.js'; + +export const MultiCallEntrypointArtifact = loadContractArtifact(MultiCallEntrypointJson as NoirCompiledContract); + +let standardContract: StandardContract; + +/** Returns the standard deployment of the multi-call entrypoint. */ +export function getStandardMultiCallEntrypoint(): Promise { + if (!standardContract) { + standardContract = makeStandardContract('MultiCallEntrypoint', MultiCallEntrypointArtifact); + } + return Promise.resolve(standardContract); +} diff --git a/yarn-project/standard-contracts/src/multi-call-entrypoint/lazy.ts b/yarn-project/standard-contracts/src/multi-call-entrypoint/lazy.ts new file mode 100644 index 000000000000..6ef1e5a45076 --- /dev/null +++ b/yarn-project/standard-contracts/src/multi-call-entrypoint/lazy.ts @@ -0,0 +1,35 @@ +import { type ContractArtifact, loadContractArtifact } from '@aztec/stdlib/abi'; + +import { makeStandardContract } from '../make_standard_contract.js'; +import type { StandardContract } from '../standard_contract.js'; + +export { + STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS, + STANDARD_MULTI_CALL_ENTRYPOINT_CLASS_ID, + STANDARD_MULTI_CALL_ENTRYPOINT_SALT, +} from './constants.js'; + +let standardContract: StandardContract; +let standardContractArtifact: ContractArtifact; + +export async function getMultiCallEntrypointArtifact(): Promise { + if (!standardContractArtifact) { + // Cannot assert this import as it's incompatible with bundlers like vite + // https://github.com/vitejs/vite/issues/19095#issuecomment-2566074352 + // Even if now supported by all major browsers, the MIME type is replaced with + // "text/javascript" + // In the meantime, this lazy import is INCOMPATIBLE WITH NODEJS + const { default: multiCallEntrypointJson } = await import('../../artifacts/MultiCallEntrypoint.json'); + standardContractArtifact = loadContractArtifact(multiCallEntrypointJson); + } + return standardContractArtifact; +} + +/** Returns the standard deployment of the multi-call entrypoint. */ +export async function getStandardMultiCallEntrypoint(): Promise { + if (!standardContract) { + const artifact = await getMultiCallEntrypointArtifact(); + standardContract = makeStandardContract('MultiCallEntrypoint', artifact); + } + return standardContract; +} diff --git a/yarn-project/standard-contracts/src/standard_contract_data.ts b/yarn-project/standard-contracts/src/standard_contract_data.ts index c6a3a5d4d126..8ba0506c45b7 100644 --- a/yarn-project/standard-contracts/src/standard_contract_data.ts +++ b/yarn-project/standard-contracts/src/standard_contract_data.ts @@ -3,22 +3,25 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { FunctionSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -export const standardContractNames = ['AuthRegistry', 'PublicChecks'] as const; +export const standardContractNames = ['AuthRegistry', 'MultiCallEntrypoint', 'PublicChecks'] as const; export type StandardContractName = (typeof standardContractNames)[number]; export const StandardContractSalt: Record = { AuthRegistry: new Fr(1), + MultiCallEntrypoint: new Fr(1), PublicChecks: new Fr(1), }; export const StandardContractAddress: Record = { AuthRegistry: AztecAddress.fromString('0x1c3bd1fbffc1a52385ed3e4bf6103bd94433b8045777902789da35bee35e4c75'), + MultiCallEntrypoint: AztecAddress.fromString('0x10b200384408725ae02f62ad68f6d43e2e737537487a0b4641cd851368a7cdd7'), PublicChecks: AztecAddress.fromString('0x23a16bc3874f7da47c13be4926eebc2a57dbba5d7cf02feb8525ed26dd0844cd'), }; export const StandardContractClassId: Record = { AuthRegistry: Fr.fromString('0x2a2197818ebc248f6933c0364e50de7b4dcee6a4e53cfadcebae98d77ec0ca4a'), + MultiCallEntrypoint: Fr.fromString('0x040a3656d743de454a3bc7ea9c4cf9d02864dbd0d79a5d1829dc197a883b32be'), PublicChecks: Fr.fromString('0x022bbd3c085d6a09ec500110852441419c7b1e6dc21a8d459233b72a84d03a1f'), }; @@ -31,6 +34,11 @@ export const StandardContractClassIdPreimage: Record< privateFunctionsRoot: Fr.fromString('0x17b584350f4c3ccafd8f688729afb9feab8976114fb40012e9dee65022c072a4'), publicBytecodeCommitment: Fr.fromString('0x2545f39893766508ce37bb5cea5e4dcab04c6f7f79f3089b1c076876e9d268b2'), }, + MultiCallEntrypoint: { + artifactHash: Fr.fromString('0x296f769c052d8f6c6e83f346e1ed0065e23e632d3e1ec530ae0b23965ac4522b'), + privateFunctionsRoot: Fr.fromString('0x0e68dfbb256e80b08b3aef47aca1f2669e97a9c6259787893c1223ac083ad5d5'), + publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), + }, PublicChecks: { artifactHash: Fr.fromString('0x030776b58475bf6a0545eaa4f4002f5fe6701bd0d306b68065f4b40ef4fdbe60'), privateFunctionsRoot: Fr.fromString('0x202860adb1b8975971eeaf571aaaa88a27f4035290d58532ae7d60b0dfaad54c'), @@ -40,6 +48,7 @@ export const StandardContractClassIdPreimage: Record< export const StandardContractInitializationHash: Record = { AuthRegistry: Fr.fromString('0x0000000000000000000000000000000000000000000000000000000000000000'), + MultiCallEntrypoint: Fr.fromString('0x0000000000000000000000000000000000000000000000000000000000000000'), PublicChecks: Fr.fromString('0x0000000000000000000000000000000000000000000000000000000000000000'), }; @@ -55,5 +64,13 @@ export const StandardContractPrivateFunctions: Record< vkHash: Fr.fromString('0x06a5c1b3a636c954a90be43cb56a4bdd9dc8aec764151a012e0018753694ff54'), }, ], + MultiCallEntrypoint: [ + { + selector: FunctionSelector.fromField( + Fr.fromString('0x00000000000000000000000000000000000000000000000000000000f04908a9'), + ), + vkHash: Fr.fromString('0x0b19b2f937f2581922c2ead5411ad9ff4ed9710efe9849bde494d9a0f94812ec'), + }, + ], PublicChecks: [], }; diff --git a/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts b/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts index c854dd077e97..30f07f9c87c2 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/bundle.ts @@ -4,9 +4,8 @@ import { StubEcdsaAccountContractArtifact, createStubEcdsaAccount } from '@aztec import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/stub/schnorr'; import type { Account, AccountContract } from '@aztec/aztec.js/account'; import type { Fq } from '@aztec/foundation/curves/bn254'; -import { getCanonicalMultiCallEntrypoint } from '@aztec/protocol-contracts/multi-call-entrypoint'; import type { ContractArtifact } from '@aztec/stdlib/abi'; -import type { CompleteAddress, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { CompleteAddress } from '@aztec/stdlib/contract'; import type { AccountType } from '../wallet_db.js'; import type { AccountContractsProvider } from './types.js'; @@ -35,8 +34,4 @@ export class BundleAccountContractsProvider implements AccountContractsProvider createStubAccount(address: CompleteAddress, type: AccountType): Promise { return Promise.resolve(type === 'schnorr' ? createStubSchnorrAccount(address) : createStubEcdsaAccount(address)); } - - getMulticallContract(): Promise<{ instance: ContractInstanceWithAddress; artifact: ContractArtifact }> { - return getCanonicalMultiCallEntrypoint(); - } } diff --git a/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts b/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts index 501212d7f7b5..178683e354c2 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/lazy.ts @@ -1,8 +1,7 @@ import type { Account, AccountContract } from '@aztec/aztec.js/account'; import type { Fq } from '@aztec/foundation/curves/bn254'; -import { getCanonicalMultiCallEntrypoint } from '@aztec/protocol-contracts/multi-call-entrypoint/lazy'; import type { ContractArtifact } from '@aztec/stdlib/abi'; -import type { CompleteAddress, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { CompleteAddress } from '@aztec/stdlib/contract'; import type { AccountType } from '../wallet_db.js'; import type { AccountContractsProvider } from './types.js'; @@ -46,8 +45,4 @@ export class LazyAccountContractsProvider implements AccountContractsProvider { return createStubEcdsaAccount(address); } } - - getMulticallContract(): Promise<{ instance: ContractInstanceWithAddress; artifact: ContractArtifact }> { - return getCanonicalMultiCallEntrypoint(); - } } diff --git a/yarn-project/wallets/src/embedded/account-contract-providers/types.ts b/yarn-project/wallets/src/embedded/account-contract-providers/types.ts index 62441a21ce1d..b1b546e160e4 100644 --- a/yarn-project/wallets/src/embedded/account-contract-providers/types.ts +++ b/yarn-project/wallets/src/embedded/account-contract-providers/types.ts @@ -1,7 +1,7 @@ import type { Account, AccountContract } from '@aztec/aztec.js/account'; import type { Fq } from '@aztec/foundation/curves/bn254'; import type { ContractArtifact } from '@aztec/stdlib/abi'; -import type { CompleteAddress, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { CompleteAddress } from '@aztec/stdlib/contract'; import type { AccountType } from '../wallet_db.js'; @@ -16,6 +16,5 @@ export interface AccountContractsProvider { getEcdsaRAccountContract(signingKey: Buffer): Promise; getEcdsaKAccountContract(signingKey: Buffer): Promise; getStubAccountContractArtifact(type: AccountType): Promise; - getMulticallContract(): Promise<{ instance: ContractInstanceWithAddress; artifact: ContractArtifact }>; createStubAccount(address: CompleteAddress, type: AccountType): Promise; } diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index cb24fc0152d2..db86511408d7 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -284,6 +284,16 @@ export class EmbeddedWallet extends BaseWallet { await this.pxe.registerContract({ instance, artifact }); } + async registerMultiCallEntrypoint( + getStandardMultiCallEntrypoint: () => Promise<{ + instance: ContractInstanceWithAddress; + artifact: ContractArtifact; + }>, + ): Promise { + const { instance, artifact } = await getStandardMultiCallEntrypoint(); + await this.pxe.registerContract({ instance, artifact }); + } + /** * Builds contract overrides for all provided addresses by replacing their account contracts with stub implementations. * Uses a type-specific stub artifact so that the stub's constructor selector matches the real account's constructor. diff --git a/yarn-project/wallets/src/embedded/entrypoints/browser.ts b/yarn-project/wallets/src/embedded/entrypoints/browser.ts index d53ae7d6b1b1..a7cafcc03807 100644 --- a/yarn-project/wallets/src/embedded/entrypoints/browser.ts +++ b/yarn-project/wallets/src/embedded/entrypoints/browser.ts @@ -71,9 +71,13 @@ export class BrowserEmbeddedWallet extends EmbeddedWallet { const walletDB = new WalletDB(walletDBStore, rootLogger.createChild('wallet:db').info); const wallet = new this(pxe, aztecNode, walletDB, new LazyAccountContractsProvider(), rootLogger) as T; - await wallet.initStubClasses(); const { getStandardAuthRegistry } = await import('@aztec/standard-contracts/auth-registry/lazy'); - await wallet.registerAuthRegistry(getStandardAuthRegistry); + const { getStandardMultiCallEntrypoint } = await import('@aztec/standard-contracts/multi-call-entrypoint/lazy'); + await Promise.all([ + wallet.initStubClasses(), + wallet.registerAuthRegistry(getStandardAuthRegistry), + wallet.registerMultiCallEntrypoint(getStandardMultiCallEntrypoint), + ]); return wallet; } } diff --git a/yarn-project/wallets/src/embedded/entrypoints/node.ts b/yarn-project/wallets/src/embedded/entrypoints/node.ts index cda1f008ac8e..275c4c216708 100644 --- a/yarn-project/wallets/src/embedded/entrypoints/node.ts +++ b/yarn-project/wallets/src/embedded/entrypoints/node.ts @@ -4,6 +4,7 @@ import { createStore, openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { type PXEConfig, getPXEConfig } from '@aztec/pxe/config'; import { type PXE, type PXECreationOptions, createPXE } from '@aztec/pxe/server'; import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry'; +import { getStandardMultiCallEntrypoint } from '@aztec/standard-contracts/multi-call-entrypoint'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { BundleAccountContractsProvider } from '../account-contract-providers/bundle.js'; @@ -79,8 +80,11 @@ export class NodeEmbeddedWallet extends EmbeddedWallet { const walletDB = new WalletDB(walletDBStore, rootLogger.createChild('wallet:db').info); const wallet = new this(pxe, aztecNode, walletDB, new BundleAccountContractsProvider(), rootLogger) as T; - await wallet.initStubClasses(); - await wallet.registerAuthRegistry(getStandardAuthRegistry); + await Promise.all([ + wallet.initStubClasses(), + wallet.registerAuthRegistry(getStandardAuthRegistry), + wallet.registerMultiCallEntrypoint(getStandardMultiCallEntrypoint), + ]); return wallet; } } diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 8906c9013283..ccdff094ecfd 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -772,6 +772,7 @@ __metadata: "@aztec/sequencer-client": "workspace:^" "@aztec/simulator": "workspace:^" "@aztec/slasher": "workspace:^" + "@aztec/standard-contracts": "workspace:^" "@aztec/stdlib": "workspace:^" "@aztec/telemetry-client": "workspace:^" "@aztec/validator-client": "workspace:^" @@ -1285,6 +1286,7 @@ __metadata: "@aztec/constants": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/protocol-contracts": "workspace:^" + "@aztec/standard-contracts": "workspace:^" "@aztec/stdlib": "workspace:^" "@jest/globals": "npm:^30.0.0" "@types/jest": "npm:^30.0.0" @@ -1894,6 +1896,7 @@ __metadata: "@aztec/noir-types": "workspace:*" "@aztec/protocol-contracts": "workspace:^" "@aztec/simulator": "workspace:^" + "@aztec/standard-contracts": "workspace:^" "@aztec/stdlib": "workspace:^" "@aztec/world-state": "workspace:^" "@jest/globals": "npm:^30.0.0"