Skip to content

[BUG] Can't send noted owned by me to other users #19900

@nchamo

Description

@nchamo

What are you trying to do?

I'm trying to build a battleships game in aztec.nr. You can see the current status here

The aztec.nr tests worked, but the UI didn't. I then built a typescript test to try to figure out the issue faster. It passed when I only had one wallet but when I refactored the test to use two wallets and I started getting the same error as the UI:

Error: No public key registered for address 0x2cd29c5d4b53dbd2a1bbd09dc110fb82946e51bbdf21ef9f485ed1b567796ad1.
        Register it by calling pxe.addAccount(...).
See docs for context: https://docs.aztec.network/developers/resources/debugging/aztecnr-errors#simulation-error-no-public-key-registered-for-address-0x0-register-it-by-calling-pxeregisterrecipient-or-pxeregisteraccount
Caused by: Error: No public key registered for address 0x2cd29c5d4b53dbd2a1bbd09dc110fb82946e51bbdf21ef9f485ed1b567796ad1.
        Register it by calling pxe.addAccount(...).
See docs for context: https://docs.aztec.network/developers/resources/debugging/aztecnr-errors#simulation-error-no-public-key-registered-for-address-0x0-register-it-by-calling-pxeregisterrecipient-or-pxeregisteraccount
 ❯ UtilityExecutionOracle.getCompleteAddress node_modules/@aztec/pxe/dest/contract_function_simulator/oracle/utility_execution_oracle.js:145:19
 ❯ node_modules/@aztec/simulator/dest/private/acvm/acvm.js:33:23
 ❯ acvm node_modules/@aztec/simulator/dest/private/acvm/acvm.js:13:36
 ❯ SimulatorRecorderWrapper.#simulate node_modules/@aztec/simulator/dest/private/circuit_recording/simulator_recorder_wrapper.js:25:22
 ❯ ContractFunctionSimulator.runUtility node_modules/@aztec/pxe/dest/contract_function_simulator/contract_function_simulator.js:144:41
 ❯ node_modules/@aztec/pxe/dest/pxe.js:677:17
 ❯ node_modules/@aztec/pxe/dest/pxe.js:149:32
 ❯ node_modules/@aztec/foundation/dest/queue/serial_queue.js:58:33
 ❯ FifoMemoryQueue.process node_modules/@aztec/foundation/dest/queue/base_memory_queue.js:110:17

Code Reference

I managed to reduce the workflow to the following steps so that it failed

  1. Host creates game
  2. Guest joins
  3. Host calls utility get_status and then I guest the same error as before
    It was weird to get that error on a utility function, but then I started digging in. Looking at the code and what the code path looked like, I realized that before the utility function is processed, private notes are synced. Then I realized that if I removed this code from the contract when the guest joined, it stopped failing:
// Store my turn and send it to my opponent
self.storage.private_turns
  .at(game_id)
  .at(turn)
  .at(player)
  .initialize(PlayedTurnNote { shot, timestamp })
  .deliver_to(opponent, MessageDelivery.ONCHAIN_CONSTRAINED);

This code basically creates a note for the guest that specifies where the shot was and sends it to the host. I continued to investigate and this is what I found:
Basically when a new note is processed, it calls attempt_note_discovery which calls attempt_note_nonce_discovery which calls compute_note_hash_and_nullifier which calls note.compute_nullifier_unconstrained

And for "simple" notes, this is the default implementation:

  unconstrained fn compute_nullifier_unconstrained(
    self,
    owner: aztec::protocol_types::address::AztecAddress,
    note_hash_for_nullification: Field,
  ) -> Field {
    let owner_npk_m = aztec::keys::getters::get_public_keys(owner).npk_m;
    // We invoke hash as a static trait function rather than calling owner_npk_m.hash() directly
    // in the quote to avoid "trait not in scope" compiler warnings.
    let owner_npk_m_hash = aztec::protocol_types::traits::Hash::hash(owner_npk_m);
    let secret = aztec::keys::getters::get_nsk_app(owner_npk_m_hash);
    aztec::protocol_types::hash::poseidon2_hash_with_separator(
      [note_hash_for_nullification, secret],
      aztec::protocol_types::constants::DOM_SEP__NOTE_NULLIFIER as Field,
    )
  }

The problem was that the host didn't have the guests (owner) keys, and that when it failed (on the first line).

Claude suggested using [custom_note] instead of [note] and implementing it like this:

impl NoteHash for PlayedTurnNote {
  
    unconstrained fn compute_nullifier_unconstrained(
        self,
        _owner: AztecAddress,
        note_hash_for_nullification: Field,
    ) -> Field {
        // For unconstrained nullifier computation (used during nonce discovery by recipients
        // who may not have the owner's keys), we use a deterministic computation based on
        // the note hash. This allows the note to be processed without requiring owner's keys.
        // Note: This means the actual nullifier will differ from this when the real owner
        // nullifies the note, but that's fine because nonce discovery just needs a consistent
        // value to identify the note.
        poseidon2_hash_with_separator(
            [note_hash_for_nullification, 0],
            DOM_SEP__NOTE_NULLIFIER as Field,
        )
    }
}

When I did that, the test started passing. You can see the change here

While the test passed, I believe that the correct behavior would be to be able to share a note, even if it's owned my me

Aztec Version

4.0.0-nightly.20260122

OS

MacOs

Browser (if relevant)

No response

Node Version

24.13.0

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-bugType: Bug. Something is broken.from-communityThis originated from the community :)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions