feat!: auto-enqueue public init nullifier for contracts with public functions#20775
feat!: auto-enqueue public init nullifier for contracts with public functions#20775nchamo merged 25 commits intomerge-train/fairiesfrom
Conversation
noir-projects/noir-contracts/contracts/test/updatable_contract/src/main.nr
Show resolved
Hide resolved
| ); | ||
| } | ||
|
|
||
| if f.name() == quote { __emit_public_init_nullifier } { |
There was a problem hiding this comment.
We are now adding this check and making sure that end users can't write a function with this name. Are we ok with this?
There was a problem hiding this comment.
Yes, this check is a good idea. Are we following this pattern elsewhere?
This is the kind of error that should have a link to the docsite with an error code.
There was a problem hiding this comment.
I don't think we are ever checking a name as the only external functions we are generating are _compute_note_hash_and_nullifier, process_message, sync_state and public_dispatch and for the first 3 we generate them only if they don't exist in the contract (to allow devs to have custom implementations) and for public dispatch the compiler catches it on its own:
Sounds like a good idea to also add here a check for the public dispatch name collision to get the same kind of error message.
There was a problem hiding this comment.
@nventuro, are we doing that somewhere else so that I can understand how to properly link to docs with an error code?
There was a problem hiding this comment.
As discussed in #20893, I don't like the mechanism of not injecting if the fn name already exists - it's too implicit. I'm in favor of erroring if the user defines, and providing an explicit mechanism for an alterantive impl.
| // No dispatch function if there are no public functions | ||
| quote {} | ||
| } else { | ||
| // If the module has an initializer, add a dispatch case for the auto-generated |
There was a problem hiding this comment.
This would the the auto generated function that set the public initializer nullifier. I tried different approaches and this one was the one that required the less changes. I don't love it if I'm being honest, so I'm more than open to suggestions
There was a problem hiding this comment.
Why not simply create an external only self function and add it to the contract? This is much more fragile
There was a problem hiding this comment.
Sadly, it's not that simple. I explained the issue in generate_injected_public_fns
yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts
Outdated
Show resolved
Hide resolved
yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts
Outdated
Show resolved
Hide resolved
nventuro
left a comment
There was a problem hiding this comment.
Good first pass! Left some comments re. docs, implemantation notes and tone.
This needs to be accompanied with some blurb somewhere explaining what these things do, e.g. in the docs for the initializer fn or in some section (which is e.g. where the error codes would link, and where we'd explain that initializing pub storage from a pub init fn requires only self). You can do a simple braindump if you want and I'll do a proper pass with the current style etc later.
Also, why did you tag the PR as a breaking change?
noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr
Outdated
Show resolved
Hide resolved
noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr
Outdated
Show resolved
Hide resolved
noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr
Outdated
Show resolved
Hide resolved
noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr
Show resolved
Hide resolved
noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr
Show resolved
Hide resolved
noir-projects/noir-contracts/contracts/test/stateful_test_contract/src/main.nr
Outdated
Show resolved
Hide resolved
noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr
Outdated
Show resolved
Hide resolved
yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts
Outdated
Show resolved
Hide resolved
yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts
Outdated
Show resolved
Hide resolved
|
@nventuro , I marked this as a breaking change because contracts that previously enqueued calls to public non-only_self functions as part of the private initializer would now stop working Basically what happened in |
benesjan
left a comment
There was a problem hiding this comment.
Both Nicos are very smart, glorious and beautiful
| ); | ||
| } | ||
|
|
||
| if f.name() == quote { __emit_public_init_nullifier } { |
There was a problem hiding this comment.
I don't think we are ever checking a name as the only external functions we are generating are _compute_note_hash_and_nullifier, process_message, sync_state and public_dispatch and for the first 3 we generate them only if they don't exist in the contract (to allow devs to have custom implementations) and for public dispatch the compiler catches it on its own:
Sounds like a good idea to also add here a check for the public dispatch name collision to get the same kind of error message.
noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr
Outdated
Show resolved
Hide resolved
…ublic-init-nullifier # Conflicts: # docs/docs-developers/docs/resources/migration_notes.md # noir-projects/aztec-nr/aztec/src/macros/aztec.nr
noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr
Outdated
Show resolved
Hide resolved
| bot.updateConfig({ | ||
| l2GasLimit: Number(totalManaUsed), | ||
| daGasLimit: Number(totalManaUsed), | ||
| daGasLimit: DEFAULT_DA_GAS_LIMIT, |
There was a problem hiding this comment.
Change discussed with @spalladino to fix an unrelated error on the CI
|
|
||
| **How initializers work:** | ||
|
|
||
| - **Private initializers** emit the private init nullifier. If the contract has any public functions, the protocol auto-enqueues a public call to emit the public init nullifier. |
There was a problem hiding this comment.
| - **Private initializers** emit the private init nullifier. If the contract has any public functions, the protocol auto-enqueues a public call to emit the public init nullifier. | |
| - **Private initializers** emit the private init nullifier. If the contract has any external public functions, the protocol auto-enqueues a public call to emit the public init nullifier. |
|
|
||
| **`only_self` functions no longer have init checks.** They behave as if marked `noinitcheck`. | ||
|
|
||
| **External functions called during initialization must be `#[only_self]`.** Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via `enqueue_self` or `call_self`) during initialization will fail the init check unless they skip it. |
There was a problem hiding this comment.
| **External functions called during initialization must be `#[only_self]`.** Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via `enqueue_self` or `call_self`) during initialization will fail the init check unless they skip it. | |
| **External functions called during private initialization must be `#[only_self]`.* |
|
|
||
| **External functions called during initialization must be `#[only_self]`.** Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via `enqueue_self` or `call_self`) during initialization will fail the init check unless they skip it. | ||
|
|
||
| **Breaking change for deployment:** If your contract has public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass `skipClassPublication: true`, because the auto-enqueued public call requires the class to be available. |
There was a problem hiding this comment.
| **Breaking change for deployment:** If your contract has public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass `skipClassPublication: true`, because the auto-enqueued public call requires the class to be available. | |
| **Breaking change for deployment:** If your contract has external public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass `skipClassPublication: true`, because the auto-enqueued public call requires the class to be available. |
| /// Returns `true` if the module has any public functions that require initialization checks (i.e. that don't have | ||
| /// `#[noinitcheck]`). If all public functions skip init checks, there's no point emitting the public init nullifier | ||
| /// since nothing will check it. |
There was a problem hiding this comment.
If we do this, it then means that mark_as_initialized_public is a dangerous functiont hat should never called manually. All of the functions in that mod are pub because technically they are in user code (used by macros). We should make it clear in their docs that they should not be used unless you know very well what you're doing.
| // Two separate init nullifiers exist because private nullifiers are committed before public execution begins. If a | ||
| // private initializer emitted a single nullifier and an initializer function enqueued a public call to do public | ||
| // initialization, then other previously enqueued public functions would observe the initialization nullifier as | ||
| // existing _before_ the public initialization code ran. | ||
| // | ||
| // Private initializers emit only the private nullifier, then enqueue `__emit_public_init_nullifier` (an auto-generated | ||
| // external public function) so the public nullifier is emitted in public. Public initializers emit both nullifiers | ||
| // directly via `mark_as_initialized_from_public_initializer`. |
There was a problem hiding this comment.
I'd rather not have a duplicate description. I think this is a bit more detailed in some respects than the apiref docs for the init fn, so I'd merge this with that, put it there and remove from here.
| // TODO(F-194): This leaks whether a contract has been initialized, since anyone who knows the address can compute this | ||
| // nullifier and check for its existence | ||
| fn compute_private_init_nullifier(address: AztecAddress) -> Field { | ||
| address.to_field() |
There was a problem hiding this comment.
(not just that, this is also not domain separated)
| fn public_fn_init_check_write_value(owner: AztecAddress, value: Field) { | ||
| self.storage.public_values.at(owner).write(value); | ||
| } | ||
|
|
||
| #[external("public")] | ||
| #[noinitcheck] | ||
| #[view] | ||
| fn public_fn_no_init_check_read_value(owner: AztecAddress) -> pub Field { | ||
| self.storage.public_values.at(owner).read() |
There was a problem hiding this comment.
Why not call these 'pub_no_init_check' and 'pub_init_check'? It'd make these tests easier to read.
|
❌ Failed to cherry-pick to |
Resolved conflicts in: - docs/docs-developers/docs/resources/migration_notes.md - noir-projects/aztec-nr/aztec/src/macros/aztec.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr - yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts
Resolved conflicts in: - docs/docs-developers/docs/resources/migration_notes.md - noir-projects/aztec-nr/aztec/src/macros/aztec.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr - yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts
BEGIN_COMMIT_OVERRIDE fix: skip oracle version check for pinned protocol contracts (#21349) fix: not reusing tags of partially reverted txs (#20817) feat: move storage_slot from partial commitment to completion hash (#21351) feat: offchain reception (#20893) fix: handle workspace members in needsRecompile crate collection (#21284) fix(aztec-nr): return Option from decode functions and fix event commitment capacity (#21264) fix: handle bad note lengths on compute_note_hash_and_nullifier (#21271) fix: address review feedback from PRs #21284 and #21237 (#21369) fix: claim contract & improve nullif docs (#21234) feat!: auto-enqueue public init nullifier for contracts with public functions (#20775) fix: search for all note nonces instead of just the one for the note index (#21438) fix: set anvilSlotsInAnEpoch in e2e_offchain_payment to prevent finalization race (#21452) fix: complete legacy oracle mappings for all pinned contracts (#21404) fix: correct inverted constrained encryption check in message delivery (#21399) feat!: improve L2ToL1MessageWitness API (#21231) END_COMMIT_OVERRIDE
Resolved conflicts in: - docs/docs-developers/docs/resources/migration_notes.md - noir-projects/aztec-nr/aztec/src/macros/aztec.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr - yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts
Closes F-239
Why we are doing this
Right now, when a contract is initialized (either privately or publicly), it emits a nullifier that is just the contract's address. The main issue with this approach is that it can lead to weird situations like the following:
In this scenario, the contract A's
should_be_invalid_but_is_notwould call contract B'spublic_callbefore contract B actually calls it's owninitialize_public. This is because:private_initializercall_b_publicwas enqueued before contract B'sinitialize_publicpublic_call's init check believes it is initializedOur fix
Our fix is quite simple. We'll use two different initializers: a private one, and a public one, and this is how it will work:
only_selffunctions do not check the initializer (i.e. they'renoinitcheck)only_selfto have these init checks, and based on our change, we decided to remove themPros
Cons
Contracts that have public external functions a private initializer now:
only_selfornoinitcheckpublic functions during initializationExtra notes
We should make utility functions check both initializers, but they check none at the time, so will do that in another PR