What is msg_sender when calling private->public? (Plus a big foray into Stealth Addresses)

Thanks to Joe, Nico, Palla, Lasse, Jan, Zac, and several others for discussion on this topic so far.

Related, but outdated and less detailed: Who is msg.sender when making private -> public function calls

This post is broken into two, because it was so massive!

This post is a deep exploration into the possible ways in which msg_sender could be “masked” when making private->public calls.

There are some approaches which could be implemented by app developers in the community, but such approaches often result in extra kernel iterations for users. There are approaches which would need to be implemented in the Aztec protocol’s kernel circuits, but we don’t understand the implications well enough to be comfortable implementing those, yet.

There’s a huge section on so-called “Stealth Addresses”, which not only enable msg_sender to be “masked”, but also unlock extra interesting private smart contract patterns that could be useful to developers. There are several approaches to stealth addresses, and each approach comes with tradeoffs.

After writing, we find ourselves unsure about which way to go in the long-term, so opinions would be very welcome! See Where to go from here? at the end.

Priv->Pub calls

There are a few scenarios in which private->public calls are made:

  • A private function of a user’s account contract calling a public function of some app.
    • At the moment, this leaks the user’s address.
  • A private function of some app contract calling a public function of some other app.
    • At the moment, this leaks the address of the calling app, and hence leaks details about the private component of the tx.
    • Sometimes, such leakage might be acceptable, sometimes it will not be. It all depends on the apps.
  • An internal call: a private function of an app making an internal call to an internal public function of the same app.
    • This privacy leakage is unavoidable.

Summary table of options for hiding msg_sender:

Here’s a pretty table. More detail on each scheme is given in subsections below.

In this doc, the term “bookkeeping” is used to mean: the logic to privately ascribe state to the correct underlying user’s address, even if in public-land all state is being ascribed to a single VPN-like contract address.

Router Router with batch function processing Deploy an ephemeral caller contract Private internal route msg_sender = null Mask msg_sender with hash Mask msg_sender, stealth- address- style
Requires extra kernel recursions Yes :cross_mark: Yes :cross_mark: Yes :cross_mark: Yes❌ No :white_check_mark: No :white_check_mark: No :white_check_mark:
# extra kernel recursions for N priv->pub calls N 1 N N 0 0 0
Requires kernel changes No :white_check_mark: Yes :cross_mark: :white_check_mark: No :white_check_mark: Yes :cross_mark: Yes :cross_mark: Yes :cross_mark:
Difficulty / complexity of making kernel change - High. It feels like quite a big increase to attack surface. - - Easy / Simple Medium Medium- Hard
Comment on most difficult aspects Private bookkeeping logic “Reset circuit” functionality for app functions (pulling target-siloed args from data bus). Tracking the ephemeral contract data Private bookkeeping logic Public function access control logic (can’t rely on msg_sender- based checks). Keeping track of the masking secrets. Keeping track of the masking secrets.
Spending notes.
Where is extra private bookkeeping? (Tracking of addresses / secrets / ids / sessions) Router Router Original caller contract Target app - private access function ???
Target app, through non-address identifiers passed-in as args?
Caller contract Caller contract
Who bears the complexity? Router contract dev Router contract dev
& Aztec Labs (kernel circuits)
Calling contract app devs Target app devs Target app devs Calling contract app devs.
Aztec Labs (kernel, pxe, aztec_nr)
Calling contract app devs.
Aztec Labs (kernel, pxe, aztec_nr)
Is there a distinction btw a private vs public msg_sender of a public fn?
This impacts ease of use of writing a pub fn.
No :white_check_mark: No :white_check_mark: No :white_check_mark: Maybe, in the sense that all private callers would be this_ address(). Yes.
A private msg_sender is mutated to null.
Kind of. msg.sender from priv wouldn’t be a valid address. No :white_check_mark:
Can someone create a private note or message for this address? :cross_mark: :cross_mark: :white_check_mark: :cross_mark: :cross_mark: :cross_mark: :white_check_mark:
With this solution, can a user create a partial note and pass it to public-land? :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
Extra constraints when calling priv->pub +N kernel recursions.
+N router fn constraints.
+1 kernel recursions.
+1 router fn constraints.
+N kernel recursions.
+N router-style fn constraints.
+N kernel recursions.
+N router-style fn constraints.
N gates N * 75 gates
(Assuming 75-gate poseidon2)
N * 700
(See section below)
Systemic point of failure?[1] Yes :cross_mark: Yes :cross_mark: No No Not really Not really Not really

[1] Paticularly scary if such infrastructure (like a router) is built externally and without rigorous quality assurance. If the thing is a kernel change, we can probably get more comfortable with it (hence “Not really”), because obviously kernels are a systemic point of failure.


Here are some options:

Solve at the app level

Let apps figure it out through extra layers of indirection. A few approaches are explored here:

Router contract

Make all calls through a router contract, which manages the underlying callers.

The idea of a “router contract” is basically a VPN. A user of an app wants to make a public call. But instead of calling directly, they route the call through the “router contract”.

The router contract does its own internal bookkeeping. It makes a record of who called it (the msg_sender), and maybe it also tracks some kind of “session id” (tx id or defi-interaction id or whatever you want to call it). Then the router contract makes the public call.

The receiving public function sees the msg_sender as the router contract, and so will associate any public state with the router contract’s address. This means the router contract will look like some giant public whale to observers, and it will need its own internal private bookkeeping system to keep track of who actually owns the states that it is publicly associated with.

Good:

  • No enshrinement of new stuff.

Bad:

  • Possibly have to rely on an external team to build it?
  • An extra kernel recursion for every enqueued public call.
  • The router contract itself might be complex to build, and with extra constraints to track its own internal state.
  • Systemic point of failure. Suppose most of the network uses one router contract: it had better work correctly, or the majority of apps on Aztec could break.
  • Notes cannot be created for this msg_sender.

See the table at the top for a more exhaustive summary of good and bad.

Router contract with batch function processing

You know the kernel reset circuits? You know how multiple functions can enqueue requests to the databus, and once the queue is sufficiently large, a kernel reset circuit can pull multiple functions’ worth of data from the queue and process those requests?

Well, we could give app functions that same functionality (as the reset circuit). It would reduce the number of kernel iterations, because only one instance of the target function would need to be executed.

Multiple functions would make calls to Foo.foo(); their arguments would all get pushed to the databus; and Foo.foo would eventually pull all of the arguments at once from the databus.

We were previously thinking about adding such functionality to enable constrained tagging requests from all functions in the call stack to be executed once, by a single tagging contract function.

This would be a complex addition to the protocol.

Good:

  • No enshrinement of new stuff.
  • Only 1 extra kernel iteration per tx

Bad:

  • Possibly have to rely on an external team to build the router?
  • Complex “batch processing” addition to the protocol.
  • Still requires that 1 extra kernel iteration.
  • The router contract itself might be complex to build, and with extra constraints to track its own internal state.
  • Systemic point of failure. Suppose most of the network uses one router contract: it had better work correctly, or the majority of apps on Aztec could break.

See the table at the top for a more exhaustive summary of good and bad.

Deploy an ephemeral caller contract

The calling contract (e.g. some app, or a user’s account contract) “deploys” an “ephemeral” private-function-only contract each time they want to make a public call, and routes the call through that.

Recall: “deploying” an instance of a private-function-only contract doesn’t actually require a call to the “Contract Instance Deployer” to deploy anything, so this does not incur a kernel iteration to deploy, or $-costs to broadcast anything. The functions and address of the instance can just be generated locally, and then the functions can be called and executed locally. The private kernel will happily execute those functions, without doing any merkle tree membership checks on the existence of the contract instance in the nullifier tree (such checks are only required for public functions).

Good:

  • No enshrinement of new stuff.
  • The user/app controls its own mini router contract.
  • No deployment cost for private-function-only contracts.

Bad:

  • An extra kernel recursion for every enqueued public call.
  • Extra complexity for all app developers to think about when making a priv->pub call, vs a universal router contract.

See the table at the top for a more exhaustive summary of good and bad.

Apps provide a private, internal route to all their public functions.

Create a smart contract standard, where for every public function #[public] fn foo(), a private counterpart #[private] fn foo__private_access is provided, which simply forwards the call to foo. This route enables msg_sender to always be that of the target public function’s contract.

Of course, foo__private_access would need to do similar bookkeeping to the above two options (the router and the ephemeral contracts).

Good:
Bad:

  • Similar cons to the router contract and the ephemeral contract ideas:
    • An extra kernel recursion for every call to the public function.
    • The private function might be complex to build, and with extra constraints to track its own internal state.
  • Bloats every contract.
  • A weird paradigm for doing access control of public state in the public function foo, in a similar way to msg_sender = null (described below), since many times msg_sender will be this_address.
  • Pushes bookkeeping complexity to every contract with a public function.

See the table at the top for a more exhaustive summary of good and bad.

This approach is effectively similar to the “msg_sender = null” approach explained below.


msg_sender = null

Have msg_sender = null for all private->public calls. The private kernel would mutate all msg_sender values to some “null” value for all priv->pub calls.

A weird consequence of doing this would be that when designing a public function, an app developer wouldn’t be able to blindly copy “traditional solidity” patterns for function access control, which often perform checks against msg_sender. Devs would need to be mindful of this edge case: that sometimes (often, in fact) msg_sender would be “null”. When storing state against an address, lots of state would end up being stored against this “null” address. A common pattern in Solidity is for the “zero address” to be a burn address, which means the “zero address” is actually a gazillionaire, with loads of tokens, so null = 0 might be too dangerous a choice for null.

Having said all that, if null is chosen to be a valid Aztec address, perhaps from the pov of the target public function, this paradigm works as seemlessly as the above suggestions of a Router Contract or a private-internal route to all public functions. Both of those approaches also bundle a huge number of users under a single msg_sender address.

Good:

  • Simple protocol.
  • No extra kernel iterations.

Bad:

  • Weird paradigm shift, maybe?
  • Difficult for devs to do access control in public functions, maybe?

See the table at the top for a more exhaustive summary of good and bad.


Mask msg_sender

Have the kernel circuit mutate msg_sender in a universal way.

Two possible approaches:

  • masked_msg_sender = poseidon2(msg_sender, masking_secret);
  • Compute a stealth address (more on this below).

Mask msg_sender through hashing

masked_msg_sender = poseidon2(msg_sender, masking_secret);

Good:

  • Simple.
  • No extra kernel iterations.

Bad:

  • Fiddly to keep track of every masking_secret.
  • Possibly confusing to devs, if msg_sender is being mutated.
  • A hashed address is no-longer a valid address, because its preimage is not that of an address. Therefore, it does not represent an underlying account contract, and has no keys.

See the table at the top for a more exhaustive summary of good and bad.

Mask msg_sender with Stealth-Address-style derivation

:::danger
This section needs a cryptographic proof, before we do anything with the idea.
:::

The hashing computation in the section immediately above yeilds a field element which is not a valid Aztec address.

Can we get a valid masked address?

Yes, the Kernel can derive a stealth address from msg_sender and some randomness.

Tl;dr:

masked_msg_sender = (msg_sender.to_address_point() + h(S) * G).x

where, generally for stealth addresses created by someone else on your behalf, S is a shared secret.

Constraint-wise, this is:

  • msg_sender.to_address_point(): 4
  • h(S): ~75
  • + h(S) * G: ~578

So ~700 gates to mask an address in this way.

(See the next subsections for more details).

Good

  • No extra kernel iterations.
  • Users can interact with this msg_sender like any other address.
  • Unlocks interesting flows (see the partial notes discussion section below).

Bad

  • Enshrinement of another thing.
  • The act of spending from a stealth address imposes extra steps (~(75 + 44) = ~120 gates).
  • Fiddly to keep track of every “masking secret” S.
  • Possibly confusing to devs, if msg_sender is being mutated.

See the table at the top for a more exhaustive summary of good and bad.

Are stealth addresses useful?

A user or an app makes a priv->pub call. Their (masked) address is now associated with some state in public-land.

Q: Why is it useful for the address to be a valid, stealth address?

A: Well, events and notes can then be sent to that address, and the original user can decrypt them.

Q: But encryption in public-land doesn’t make sense; encryption needs to happen in a private function.

A: Sure, but whoever wants to send state (or an event) to this (masked) address can do so via a private function.

Crucially, stealth-address-style masking broadcasts a proper address out into the world, for people to interact with in any number of ways, via private functions. Those interactions don’t even need to be part of the same app as the original priv->pub call.

A noddy example is airdrops (thanks Nico): Some team wants to do an airdrop to all users of an app. Ok, they can do that now, because they have valid, (masked) addresses for all users, so they can send those users notes in a different app.

See also #Stealth-Addresses-vs-Partial-Notes below.

Steath Address Derivation

Nitty gritty section.

Before starting: there are probably 100s of different possible address and stealth address derivation schemes. We’ll need to stop exploring eventually, or we won’t ship. Below are some of the most “obvious” schemes and their tradeoffs.

Aztec Address - Reminder

Let’s remind ourselves how an address is derived. Here’s a screenshot from an old presentation:

So:

// Several ways of describing AddressPoint derivation:
AddressPoint = h(partial_address, h(Ivpk, Npk))*G + ivsk*G
             = pre_address*G + ivsk*G
             = (pre_address + ivsk)*G
             = address_sk*G
             = pre_address*G + Ivpk

address = AddressPoint.x; // x-coord

where pre_address (for want of a better name) (h in the pic above) is a hash of all the contract bytecode stuff and public keys stuff, and ivsk is the incoming viewing secret key for the public key Ivpk.

Of particular importance: pre_address = hash(partial_address, hash(ivsk * G, Npk)).

Deriving pre_address from ivsk in this way cements this ivsk as the only possible ivsk of this address, to prevent malleability attacks on pre_address + ivsk, where an attacker could choose any two addends they like.

You can think of (pre_address + ivsk) as the address_secret_key.

If we define a stealth address in a similar way to the Ethereum stealth addresses EIP:

Let S be some ECDH shared secret. (Depending on the use case, it doesn’t necessarily need to be a shared secret if the use case is “singleplayer”).

Then define

// Several ways of describing AddressPoint derivation:
StealthAddressPoint = AddressPoint + h(S)*G
                    = pre_address*G + Ivpk + h(S)*G
                    = pre_address*G + ivsk*G + h(S)*G
                    = (pre_address + (ivsk + h(S))) * G
                    = stealth_address_sk*G

stealth_address = StealthAddressPoint.x // x-coord

This stealth address has a secret key:

stealth_address_sk = pre_address + ivsk + h(S) = pre_address + (ivsk + h(S)).

This is basically the same as the original address_sk, but with that h(S) term added.

Since the owner of AddressPoint is the only entity who can derive stealth_address_sk (since only they know ivsk), if someone encrypts to this StealthAddressPoint, the owner of the original address can decrypt the ciphertexts.

Notice that the pre_address of AddressPoint and StealthAddressPoint is the same, so if you squint, they both represent the same contract bytecode, and they almost represent the same hard-coded public keys.

That is, the StealthAddressPoint does map to a valid contract, and it’s the same contract as the original AddressPoint.

Problems with Stealth Addresses

We try to solve these further below.

  • Slightly different keys baked into the stealth address’ pre_address. The stealth_ivsk := ivsk + h(S) is not baked into the pre_address; ivsk is. That’s a potential problem if the owner of StealthAddressPoint ever wanted to spend their notes!
  • Spending my notes from multiple stealth addresses at once: Notes which are sent to Alice’s various stealth addresses do not end up in the same spendable “pot” of notes:
    • The notes belong to different state variables.
    • Authwit calls (for permission to edit such states) would currently need to be made to each stealth address. That’d be too much recursion.
    • See below for a solution.
  • Creating stealth addresses from stealth addresses requires a homomorphic hash. See section below. The Pedersen Hash is a homomorphic hash, but using it requires an extra field to be broadcast. What if someone creates a stealth address from your stealth address? After all, they have no idea which addresses onchain are stealth, and which are “original” addresses. Now you’ll have a doubly-stealth address of the form A + (h(S') + h(S''))*G. That’s annoying, because the hash I had in mind (poseidon2) is not homomorphic, meaning h(S' + S'') is not equal to h(S') + h(S''). This means spending from that doubly-stealth address requires different constraints from spending from a singly-stealth address. And what about a triply-stealth address or quadrupley-stealth address? It’s not feasible for a circuit to cope with all spending possibilities, if h() is not homomorphic.

Spending Notes from multiple stealth addresses at once

Ideally, we want for notes which are sent to a user’s “original” Address, and notes which are sent to that same user’s many Stealth Addresses, to all end up in the same “bucket” of spendable notes.

The main problems we have to achieving this, are:

The notes belong to different state variables

Storage Slots:

Suppose Alice receives notes to 3 different (but linked) addresses:

  • A - original address
  • A' = A + h(S')*G
  • A'' = A + h(S'')*G

Then unfortunately, those 3 notes will be stored in 3 different state variables: balances[A], balances[A'], balances[A''].

Those state variables have different storage_slots: call them: slotA = h(balances.slot, A), slotA' = h(balances.slot, A'), slotA'' = h(balances.slot, A'').

This means each of the 3 notes contains a different storage_slot in its preimage: slotA, slotA', slotA''.

So given how Aztec_nr is currently designed, if Alice wanted to “Get all notes for balances[A]”, she’d only receive 1 of her 3 notes, and she’d also only be able to demonstrate (to aztec_nr) that 1 of her 3 notes belongs to address A.

But what we actually want to happen is for Alice to say “Get all notes for balances[A]”, and for Alice to receive all 3 of her notes, and also to be able to demonstrate (to aztec_nr) that all 3 of her notes belong to address A.

Let’s look at some low-level pseudocode for how this might look:


// Get me all notes belonging to address A:
let notes = balances[address].get_notes();

let A = address.to_address_point();

for note in notes {
    /********
     * This is the only part that's different versus current aztec_nr
     * when we consider extra constraints to cope with stealth addresses!!!
     ********/
    
    // Hand-wavy: we need a way to hint the S values.
    // 
    // Note also: these might need to be siloed per contract address, 
    // to avoid malicious contracts from linking your
    // stealth addresses with your main address (by making
    // calls to this oracle)!
    // That might diminish the usefulness of stealth addresses,
    // if you can't use the same one across apps. We'll see.
    let is_stealth_address: bool = oracle.is_stealth_address(note.owner);
    
    let A' = if is_stealth_address {
        let S' = oracle.get_stealth_address_shared_secret(note.owner);
        // An extra ~600 constraints for the ECADD & ECMUL
        A + h(S') * G // elliptic curve add & mul.
    else {
        A
    }
    
    /********
     * End of different part.
     * Everything below is effectively the same as what aztec_nr currently does.
     ********/
    
    let slotA' = h(balances.slot, A');
    
    assert(note.owner == A'.x);
    
    assert(note.storage_slot == slotA');
    
    // The above assertions demonstrate that the owner of 
    // original address A is also the owner of A',
    // and that notes which belong to slotA' also belong
    // to A.
    
    // These aren't what the aztec_nr functions are called.
    // It's pseudocode to show you what's happening.
    
    // Reconstruct the address from the Npk, to prove the Npk is correct.
    let { Ivpk, Npk } = oracle.get_master_public_keys(A);
    let partial_address = oracle.get_partial_address(A);
    let pre_address = hash(partial_address, hash(Ivpk, Npk));
    assert(A == pre_address * G + Ivpk);
    
    // Now demonstrate knowledge of nsk_app, relative to Npk.
    let nsk_app = oracle.get_nsk_app(Npk);
    // This call safely proves the link between nsk_app and Npk,
    // in the Kernel circuit (since only the kernel may know nsk):
    // assert(Npk == nsk * G);
    // assert(nsk_app == h(nsk, app_contract_address));
    // And hence they're linked.
    context.push_key_validation_request(Npk, nsk_app);
    
    // And hence I know the nsk_app for address A, so I can go ahead and nullify the note.
    
    let note_hash = h(...note);
    let nullifier = h(note_hash, nsk_app);
    
    context.push_new_nullifier(nullifier);
}

Authwit calls would currently need to be made to each stealth address

The above code snippet also solves this problem. Above, we demonstrated the link between A and any stealth address A'. So now the contract can happily make an authwit call solely to the “original” account contract A.

With the code above, an app contract would never be “tricked” into calling a stealth address, because A is derived differently from all A'.

Unfortunately, if some other user needs to make an authwit call to your stealth address, you would need to show them the derivation from your original address, so that they may demonstrate that the correct bytecode is being used. That kind of defeats the purpose of stealth addresses. Even if you could mask the bytecode with a salt (see “salty stealth addresses” below), there’s still that + Ivpk term in the stealth address derivation that leaks the original owner.

Hmmm… that makes me wonder if we can re-randomise Ivpk when stealthifying an address…

Re-randomising Ivpk

Maybe skip this subsection.

Summary

Reminding ourselves of what we have:

A = h(partial_address, h(Ivpk, Npk))*G + Ivpk
  = pre_address*G + Ivpk

Let’s use the random shared secret S' to randomise the Ivpk.

How about:

A' = h(S')*h(partial_address, h(Ivpk, Npk))*G + h(S')*Ivpk + h(S')*G

Whilst this enables someone to compute a stealth address for you, from A, another person who wishes to call this stealth address would still be able to see the keys_hash = h(Ivpk, Npk) when computing the pre_address, so it doesn’t solve our problem.

The caller would also need to know h(S') and using that they could reverse-derive the Ivpk from h(S')*Ivpk, so this is terrible.

This also feels like it might have malleability problems, enabling an attacker with two such stealth addresses to learn the underlying A. And possibly if you tried to sign with this address, an attacker might be able to forge signatures. TBD.

How about:

A' = h(partial_address, h(h(S')*Ivpk, h(S')*Npk))*G + h(S')*Ivpk + h(h(h(S')))*G

Someone else would not be able to compute this from A alone, which is a shame; they’d need the underlying address preimage data.

The triple hash is to prevent others from learning h(S') (only the owner and creator of the stealth address would know it).

How about re-defining A, for fun (you’re having fun, right?):

A = contract_class_id*G1 + initialisation_hash*G2 + secret_salt*G3 + h(Npk)*G4 + Ivpk 
   = contract_class_id*G1 + Stuff

A stealth address is generated by swapping the secret_salt to secret_salt':

A' = contract_class_id*G1 + initialisation_hash*G2 + secret_salt'*G3 + h(Npk)*G4 + Ivpk
   = contract_class_id*G1 + Stuff'

This completely changes the tradeoffs, and I’m probably not being serious here. It does tick some cool boxes, though:

  • A caller of this A' only learns the contract_class_id, and not other revealing information about the original address (since Stuff' is a hiding Pedersen commitment).
  • Someone can create a stealth address A' from A.
  • Someone can create a stealth address from a stealth address (more on this in the next subsection).

The worst thing about this is: an ephemeral public key to encrypt to such an address A would be 5 compressed elliptic curve points, which is unacceptably big.

That subsection was pretty disgusting and hastily explained. I rushed it, so the above might not work. I’ll need to think about it for longer. Let’s forget the suggestions of that section and move on.

Creating stealth addresses from stealth addresses requires a homomorphic hash

No one will have any idea which addresses onchain are stealth, and which are “original” addresses. That’s the whole point.

But what if someone creates a stealth address from your stealth address?

A'' = A' + h(S'')*G

Now you’ll have a “doubly-stealth” address. If you were to try to derive that address from the “original” address A, you’d do:

A'' =  A'           + h(S'')*G
    = (A + h(S')*G) + h(S'')*G
    = A + (h(S') + h(S''))*G

That’s annoying, because the hash I had in mind (poseidon2) is not homomorphic, meaning h(S' + S'') is not equal to h(S') + h(S''). This means spending from that doubly-stealth address requires different constraints from spending from a singly-stealth address. (It requires the spender to compute (h(S') + h(S'')) instead of h(something)):

A'  = A + h(S')*G // one hash

A'' = A + (h(S') + h(S''))*G // two hashes. sad face.

And what about a triply-stealth address or quadrupley-stealth address? It’s not feasible for a smart contract to compile various “sizes” of the same function to cope with all spending possibilities, if h() is not homomorphic.

Pedersen is homomorphic

Instead of using h(S'), we can change the derivation of a stealth address to be:

Let S' be the ecdh shared secret, as normal. Let s' = S'.x; the x-coordinate of S'.

A' = A + s' * H for a new elliptic curve generator point H.

This works (it gives us a homomorphic stealth address), because the stealth address becomes Pedersen-commitment-like. But it’s really annoying and results in an extra field being broadcast if you want to encrypt to this stealth address definition.

Why does it work?

A'' =  A'        + s''*H
    = (A + s'*H) + s''*H
    = A + (s' + s'')*H

Hooray! A person can create a stealth address A'' from a stealth address A', and the owner of the original address can prove the derivation of A'' from A with the same constraints as proving the derivation of A' from A:

A'  = A + s'*H
A'' = A + t *H // where t = s' + s''. Same constraints to derive both A' and A'' from A!

So the circuit which spends notes owned by any of A, A', A'' can do so with the same stealth address derivation constraints.

Malleability

Why did we need to introduce H? Why can’t we just use G (without the hash h(S') and have a stealth address be defined as:

// Bad definition
A' = A + s'*G

It’s because of malleability.

Let’s write out the full derivation of A, under this “bad definition”:

// Bad definition
A' = h(partial_address, h(Ivpk, Npk))*G + ivsk*G + s'*G

Having s' “floating freely” like that (instead of h(S')*G or s'*H ) means the owner of A' can lie about their partial_address, ivsk, Ivpk, Npk. They can choose rival values ~partial_address, ~ivsk, ~Ivpk, ~Npk, and derive a malicious shared secret:

~s' = h(partial_address, h(Ivpk, Npk)) + ivsk + s' - h(~partial_address, h(~Ivpk, ~Npk)) - ~ivsk'

such that:

// Malicious derivation of bad defn of A':
A' = h(~partial_address, h(~Ivpk, ~Npk))*G + ~ivsk*G + ~s'*G

This malleability attack enables the owner of A' to:

  • Nullify a note (owned by A') ~infinite times under different nullifier keys ~Npk.
  • Demonstrate that the underlying account contract of A' is any contract they like, by providing any ~partial_address.
  • Make authwit calls to any contract they like, since they can demonstreate any account contract is theirs.
Why does s'*H result in an extra field being broadcast, when encrypting?

Ignoring stealth addresses momentarily, the reason our address scheme looks a bit covoluted is because we sought to keep an ephemeral public key (used for ecdh secret sharing) as a single compressed point (1 Field & 1 bit):

Originally:

// Old address derivation:
address = h(partial_address, h(Ivpk, Npk))

This wasn’t an elliptic curve point, so we couldn’t “encrypt to it”. If someone asked “Hey, what’s your address”, you’d need to given them address and Ivpk (otherwise they wouldn’t be able to encrypt to you), and partial_address & Npk (otherwise they wouldn’t be able to prove the correctness of Ivpk relative to address).

Then:

We considered making an address a Pedersen commitment:

// Candidate address derivation - never fleshed out - never adopted.
AddressPoint = partial_address*H + ivsk*G

But the ephemeral public key – when encrypting – would have become two compressed points:

// A sender would compute:
Eph_pk = [eph_sk * H, eph_sk * G] // two compressed points to be broadcast
Shared_secret = eph_sk * Address
   
// The recipient would compute:
Shared_secret = partial_address*Eph_pk[0] + ivsk*Eph_pk[1]
// ... which is equiv to (but the recipient doesn't know eph_sk):
              = partial_address*(eph_sk*H) + ivsk*(eph_sk*G)
              = eph_sk * Address

We didn’t like there being two compressed points being broadcast, so we didn’t finish designing that address scheme in such a way that Ivpk & Npk were constrained to be a part of the address.

Now:

We sought to use G for everything, and ended up with:

// Current address:
AddressPoint = h(partial_address, h(Ivpk, Npk))*G + ivsk*G

But:

With this stealth address proposal – of introducing s'*H – we’re in danger of broadcasting that extra pesky compressed point:

StealthAddressPoint = h(partial_address, h(Ivpk, Npk))*G + ivsk*G + s'*H

// A sender would compute:
Eph_pk = [eph_sk * G, eph_sk * H] // two compressed points to be broadcast
Shared_secret = eph_sk * StealthAddressPoint
              
Shared_secret = (h(partial_address, h(Ivpk, Npk)) + ivsk)*Eph_pk[0]   + s'*Eph_pk[1]
// ... which is equiv to (but the recipient doesn't know eph_sk):
              = (h(partial_address, h(Ivpk, Npk)) + ivsk)*(esph_sk*G) + s'*(eph_sk*H)
              = eph_sk * (h(partial_address, h(Ivpk, Npk))*G + ivsk*G + s'*H)
              = eph_sk * StealthAddressPoint

It works, we get lots of lovely properties, but for each encrypted log, the Eph_pk is 2 Fields & 2 bits.

Should all encrypted logs even use an ephemeral public key?

In cases where the sender and recipient can handshake in advance, a sequence of tags can be derived from a handshaking shared secret. I reckon the symmetric key of any ciphertexts can therefore be derived from the handshake shared secret, instead of from an ephemeral shared secret:

tag_i = h(hs_shared_secret, i); // for an incrementing index i

symmetric_key_i_j = h(hs_shared_secret, i, j); // for example - I haven't thought about this much

I guess a problem is in our security model for keys. If the handshake shared secret is leaked currently, it doesn’t enable the attacker to decrypt; it only enables the attacker to link ciphertexts to this user. The handshaking shared secret is arguably in a “less secure” environment than the ivsk (to be debated, depending on the wallet’s setup), and so is more likely to leak. Therefore, if we made this change, an attacker who learns the handshake shared secret would be able to decrypt all of the user’s notes and messages. Bad.

What happens if a stealth app or account contract is called?

There are some problems:

  • A stealth address will contain different state from the “original” address:
    • Different state updates over time.
    • Different initialisation (or possibly no initialisation at all!), but the constructor args baked into the pre_address are those inherited from the original address!!!
  • To call a stealth address requies its bytecode, which then leaks the preimage of the pre_address, which then leaks the true owner of the contract, which then invalidates the “stealthiness” of the stealth address.
  • Whiltelists/Blacklists could be circumvented.

A simple solution is to never allow calls to a stealth address.

A more complex solution (which I haven’t explored, because it’s complicated) is to somehow enable calls to stealth addresses.

Preventing calls to stealth addresses

The above big code snippet shows how one can prove that a stealth address A' relates to an original address A. The caller of a stealth address could demonstrate to the kernel (through a hint) that the target A' is derived from A, and hence A should be called.

Enabling calls to stealth addresses

? Not sure how, or of the benefit.

You might need to solve:

  • Providing the class_id and initialisation nullifier of a stealth address without leaking the keys (which are leaky) and the initialisation hash (which is also leaky unless it’s salted).
  • Enabling someone to create a stealth address for you, in such a way that the initialisation hash and keys are re-salted.
Whitelists/Blacklists could be circumvented

The big code snippet above shows logic that an app could implement if it wants to enforce a whitelist / blacklist: it can constrain that the original address is used, by checking the address’s derivation.

Diversified addresses

They don’t really solve our problems, but putting them here for completeness:

The owner (and only the owner) of an address would be able to “diversify” their address with a randomised G_d generated from G.

AddressPoint = h(partial_address, h(Ivpk, Npk))*G + ivsk*G

let G_d = d * G // for some random "diversifier" field `d`

DiversifiedAddressPoint = h(partial_address, h(Ivpk, Npk))*G_d + ivsk*G_d

It’s all kinds of weird, though, since ivsk*G_d is not equal to the Ivpk inside the hash. And if someone wants to encrypt to this diversified address, they’d need to be given d, so that they can derive G_d, so that they can derive an ephemeral public key from G_d.

Salty Stealth Addresses

Palla suggested modifying the random salt in the preimage of a user’s address, as a way to generate a stealth address.

It might help to look back at the big tree pic further up the page.

// Original address
initialisation_hash = hash(deployed_address, salt, constructor_hash);
partial_address = h(contract_class_id, initialisation_hash);
AddressPoint = h(partial_address, h(Ivpk, Npk))*G + Ivpk;

So the idea would be to change that salt value to a new salt':

// Stealth Address:
initialisation_hash' = hash(deployed_address, salt', constructor_hash);
partial_address' = h(contract_class_id, initialisation_hash');
StealthAddressPoint = h(partial_address', h(Ivpk, Npk))*G + Ivpk;

A downside of this approach is that someone can’t create a stealth adress for you, unless they’re given your address preimage data (so that they can derive a different salt).

This exercise maybe highlights an improvement that could be made to the topology of an address preimage: enabling a salt to be edited without leaking details of the original address (i.e. without leaking the initialisation data nor the keys).

Actually, regardless of preimage topology, I don’t think it’s possible to enable someone to modify the salt without learning the orginal owner, because of that free-floating + Ivpk term!

Summary of this Stealth Address section:

“Traditional stealthiness” “Pedersen-like stealthiness” “Diversified address” Stealth by re-salting the address preimage. Fully embracing Pedersen Commitments Insecure Stealth (for posterity)
Derivation A' = A + h(S')*G A' = A + s'*H A' = pre*G_d + ivsk*G_d Change salt in preimage of pre_address to get pre'.
A' = pre'*G + ivsk*G
A' = contract_class_id*G1 + initialisation_hash*G2 + secret_salt'*G3 + h(Npk)*G4 + Ivpk A' = A + s'*G
Secure? Maybe Maybe Maybe Maybe. Design underexplored. Maybe :cross_mark: Malleability: owner can cheat.
Approach already in use in other ecosystems? :white_check_mark: Same as Ethereum derivation. :cross_mark: :white_check_mark: ZCash Orchard (similar, at least). :cross_mark: :cross_mark: -
Someone can create a stealth address for you, from just your address. :white_check_mark: :white_check_mark: :cross_mark: :cross_mark: :white_check_mark: -
Someone can create a stealth address for you, from your address plus some extra data. :white_check_mark: Not needed given row above. :white_check_mark: Not needed given row above. :cross_mark: :white_check_mark: need access to the pre_address preimage. :white_check_mark: Not needed given row above. -
Someone can create “a stealth address from just a stealth address”, and constraints to spend from the doubly-stealth address are the same as a singly-stealth address. :cross_mark: They can create the stealth address, but different constraints to spend :(, so “stealthness” of an address might need to be conveyed in public (extra bit of data) to prevent creating stealth from stealth. :white_check_mark: :cross_mark: :cross_mark: Creating a stealth address requires access to the pre_address preimage, which could leak the original address, depending on the preimage topology. Regardless, modifying the salt leaks the standalone + Ivpk term, which leaks the original address’s public key. :white_check_mark: -
Notes owned by original & stealth addresses can be spent altogether (from one “pot”). :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: -
The ephemeral public key remains as 1 compressed point (saves on data in encrypted logs). :white_check_mark: :cross_mark: 2 compressed points :white_check_mark: :white_check_mark: :cross_mark: 5 compressed points (LOL) -
No extra data is needed to encrypt to the stealth address. :white_check_mark: :white_check_mark: :cross_mark: d needs to be given. :white_check_mark: :white_check_mark: -
No circumventing whitelists. :white_check_mark: Requires extra in-app logic to show stealth address derivation. Obviously, this mustn’t happen in public, or it defeats the purpose of stealth addresses. :white_check_mark: As per left. :white_check_mark: As per left. :white_check_mark: As per left. :white_check_mark: As per left. -
The stealth address can be called. Not really explored. Current mental model is that the call would be forceably routed to original address by kernel. As per left. As per left As per left As per left -
The stealth address can be called, without leaking the original address to the caller (except the contract_class_id) :cross_mark: Not achievable with that + Ivpk term in the address derivation. :cross_mark: Not achievable with that + Ivpk term in the address derivation. :cross_mark: Not achievable with that + Ivpk term in the address derivation. :cross_mark: Not achievable with that + Ivpk term in the address derivation. :white_check_mark: (Although this row contradicts the previous row: you can’t route to the oiginal address if you want to hide the original address from the caller!!!). -
Two colluding parties, each with a different “stealth addresses” of yours, cannot convince each other that they belong to you. (A zcash requirement). Depends how the stealth address is provided and used. As per left. As per left. As per left. As per left. -
Extra constraints for someone to derive the address for you. ~653 (ECADD, ECMUL, Poseidon2) plus log to share S' ~578 (ECADD, ECMUL) n/a ~800 (3x poseidon2, ECADD, ECMUL). Depends on design of preimage topology. :cross_mark: ~2k? -
Extra constraints to spend from the address. ~653 (ECADD, ECMUL, Poseidon2) ~578 (ECADD, ECMUL) ~534 (ECMUL) ~800 (3x poseidon2, ECADD, ECMUL). Depends on design of preimage topology. :cross_mark: ~2k? -
Extra constraints to call (routed to original). " " " " " -

Stealth Addresses vs Partial Notes

If the above stealth address scheme is secure (and that’s a big “if”), then we now have two interesting patterns for private/public composability: partial notes and these stealth addresses.

Partial note:

  • A user commits to {…some_note_data, randomness} in a private function.
  • Makes a priv->pub call to store the commitment (a so-called “partial note”) in public.
  • The user “subscribes” to some future event which informs them that the note has been completed.
  • Some time later… A public function (or it could be a private function):
    • adds some value(s) to the commitment (through additively-homomorphic elliptic curve stuff), to create a “completed” note;
    • emits the note;
    • emits a new event to inform the original user (and maybe others) about this.
  • The user learns about the value(s) that were added to their note.

What information does this flow seek to keep private?

  • Hides some data in the note.
    • Often this is the “owner” of the note.

We can mimic the pattern of “creating a note for some unknown owner” with Stealth Addresses:

Stealth Addresses

  • A user “puts” a stealth address in public land (as part of some larger defi flow, akin to the reasons for creating partial notes).
    • That might mean passing the stealth address as an argument to a priv->pub function.
    • It might mean making a priv->pub call from their account contract (and having the protocol mutate their address). (But of course this 2nd bullet point only works if we bake stealth address mutation of msg_sender into the kernel).
  • Some time later… A function (private or public, depending on the use case):
    • Creates a note for the stealth address.
    • Emits the note.
    • Emits a new note log.
    • Done?
    • Note: if the creator of the note needs to keep some other data fields in the note secret, they’ll need to create the note in a private function; otherwise, a public function can be used.
      • If a public function is used, the whole world learns that “This stealth address received a note containing all of this completely public information”. But in many cases that will be fine, since the stealth address doesn’t link back to the original address. A problem could arise if this one stealth address is used all over the place to represent this user. E.g. if the stealth address is used to create multiple notes, via public functions; then the stealth address effectively becomes the user’s de facto address.
      • So there’s privacy gotchas to be aware of, in the sense that a long-lived stealth address being used in the public eye might leak a tx graph.

It does seem like quite a simple, powerful primitive, though.

If stealth addresses become enshrined (or even widely used), app developers have to be careful when writing note nullification logic. As shown in the above Stealth Address Derivation section, there’s an extra line of logic needed to prove that a nsk relates to a stealth address, versus the original address.

Where to go from here?

There are actually a lot of open questions around requirements. The two big tables above outline many of the potential requirements – just look at the items in the first column.

  • How to mask msg_sender for priv->pub calls?
    • Are extra kernel recursions acceptable?
      • TODO: measure the potential slow-down for users.
    • Are some approaches too burdensome or error-prone for smart contract developers?
  • Should address mutation be into the protocol?
  • Should stealth addresses be adopted? If so, what are the requirements for the stealth address scheme? See the table for possible requirements. Important requirements to decide upon are:
    • Should someone be able to derive a stealth address for you, from just your address?
    • Should notes which are owned by many of your different stealth addresses all be spendable from the same “pot”?
    • Are Stealth Addresses useful and desired by the community?

IF YOU HAVE OPINIONS ABOUT THESE POTENTIAL REQUIREMENTS, PLEASE DO SHARE :slight_smile:

In the mean time, we’re going to start simple:

  • Build an e2e example whereby ephemeral re-salted contract instances are deployed to mask msg_sender.
  • Get accurate numbers on the cost of an extra kernel recursion.
  • Ask devs whether they want stealth addresses, or whether other patterns are preferred.
  • Observe what devs do and want.

Disclaimer

The information set out herein is for discussion purposes only and does not represent any binding indication or commitment by Aztec Labs and its employees to take any action whatsoever, including relating to the structure and/or any potential operation of the Aztec protocol or the protocol roadmap. In particular: (i) nothing in these posts is intended to create any contractual or other form of legal relationship with Aztec Labs or third parties who engage with such posts (including, without limitation, by submitting a proposal or responding to posts), (ii) by engaging with any post, the relevant persons are consenting to Aztec Labs’ use and publication of such engagement and related information on an open-source basis (and agree that Aztec Labs will not treat such engagement and related information as confidential), and (iii) Aztec Labs is not under any duty to consider any or all engagements, and that consideration of such engagements and any decision to award grants or other rewards for any such engagement is entirely at Aztec Labs’ sole discretion. Please do not rely on any information on this forum for any purpose - the development, release, and timing of any products, features or functionality remains subject to change and is currently entirely hypothetical. Nothing on this forum should be treated as an offer to sell any security or any other asset by Aztec Labs or its affiliates, and you should not rely on any forum posts or content for advice of any kind, including legal, investment, financial, tax or other professional advice.

from a user perspective, it’s interesting to see that pseudonimity is enhaced, avoiding the need of address rotation to break the link between different transactions. despite migrating account doesn’t incur in deployment costs, one does need to migrate all storage in apps that are pointing to that account into the new one (hopefully only private storage, since migrating public storage would mean shielding first to private, or losing the purpose of rotating accounts).

i understand that this feature comes to solve this, from the perspective that i can share with someone a stealth address, and he won’t be able to associate the public storage slots related to my original account. and also, because i can use priv->pub through my stealth address, i could have multiple storage slots that are ultimately handled by me (bc i could spawn priv->pub txs from any of these stealth addresses).

if notes exist for A, A’, A’‘, and (A’)‘, A should be able to nullify all at the same time (having an authwit call to A), and A’ should be able to nullify A’ and (A’)‘, yet the authwit call would fail. my question here is, let’s assume authwit call to A’ works, can B with authorization from A’ nullify A’ and (A’)’ without revealing who’s A?

from a dev perspective, calling enqueue with an extra argument to provide this stealthyness seems straightforward, no argument means msg_sender is the original, i don’t think this incurs in bigger headspace for the app developers, since from the public state, they assume that this address can call them again (and do whatever with the authenticated storage).

i’m trying to think if “creating a stealth address for someone else” is that much of a feature. if i know the someone else, i can transact in private. yet if we see the case of a merchant (some address that receives many transactions from different people), this people would want not to be linked to it (or to his other customers) and may chose to tx to a stealth address. if they can’t create one, then the merchant needs to provide it (extra steps).

for now this is just what’s at the top of my head, but will give it a further though about, thanks for the explanation!

That’s a good question! I’m not sure. We’d have figure out how to solve this if we wanted this to be a requirement.

Do you think it would be an important requirement, to enable B to make an authwit call to A’ without learning A? (I’m genuinely asking; I don’t know the answer).

If B wants to make an authwit call to A’ (pretending that this authwit would work, as you say), B would need to know the preimage of address A’, which with our current address preimage design would leak details about A. So at the moment, A would be revealed to B. To solve this, we’d need to change the layout of the preimage of an address, and might even need to re-randomise the Ivpk (which forms part of the address). There’s a badly-written rambling section above (hidden - it needs to be clicked to expand it), which touches on the difficulties of re-randomising this data. Even if we figure out how to enable B to execute the bytecode of A’ without realising that it belongs to A, we’d need to also figure out how to enable B to read state from A’ without realising that this state belongs to A.

In particular, as you say, we’d need to figure out how to safely enable an authwit call to A’, which is difficult because A’ doesn’t necessarily inherit the state of A (it would depend on how we design things). In many of the stealth address schemes explored above, A’ isn’t even “deployed”, and so isn’t “initialised” with any storage. If the authwit was a trivial “lookup the public key, then verify a signature”, the public key state would be 0 in A’ and signature verification would fail. Even if A’ did inherit the storage of A, the preimse that the state in A would be the same as the state in A’ would leak the fact that A and A’ are related. So the auth public key of A’ would probably need to be different from the auth public key of A, so that the authwit process didn’t leak “A<>A’” to B.

Perhaps all of this suggests that A’ does need its own storage to be initialised?

Interestingly, the whole notion of “stealth addresses” is borrowed from an Ethereum EIP, and they explain in their Abstract that their motivating use case is for a Sender to create a stealth address for a Recipient. So “creating a stealth address for someone else” is the original feature / requirement that stealth addresses solve.

But perhaps you’re right; given that Aztec has private funtions, this feature might be less useful:

if i know the someone else, i can transact in private

I didn’t follow this bit.

I’m trying to think of possibilities:
B knows A: B can transact with A through private functions.
A wants to call a public function: A creates a stealth address A’.
A wants to interact with B, without B learning who A is: A creates a stealth address A’.
B knows A and wants to interact with A through a public function, without the world seeing who A is: B creates a stealth address A’.

Any other possibilities?

that’s what i was asking, and specially if B wants to spend notes from A’ and (A’)', if he’ll be able to prove that they’re the same “underlying address”, without requiring to know A

in the same sense, can B provably know that the (let’s say) Plume nullifier that he got for the A’ note is valid? given that this nullifier is linked to A?

i’m not precisely concerned in B knowing the preimage of A, i imagine that there will be only a bunch of account contracts (probably related to wallet implementations), so most of the accounts will use some known contract class id, but what B shouldn’t know is what’s the actual address of A

i think this is the main use case, the stealth address is A interacting in the public space with the same underlying account, but breaking the link between the many interactions that may have

i imagine that this could also create sub-accounts in a smart (aka dumb) contract, in which you can save 1 kernel iteration when you need to isolate the balances, by adding a constrained salt in the enqueue(context, A) or enqueue(context, B), both A and B could use the same (constrained) entrypoint to interact with the public space, from the same underlying contract, but having 2 separate balances in a token (for example)

could #[internal] also allow this? instead of checking msg_sender == this_address check msg_sender ∈ this_address?

These are some interesting potential requirements that we might want to consider - thanks for thinking of them :slight_smile:

To rephrase them as user stories, for my own future reference:

  • B should be able to call A’ (for an authwit) without learning that he is calling A.
  • B should be able to spend notes from A’ and (A’)’ at the same time, by proving they share the same “underlying address”, without learning that they relate to A.
  • B should be able to (plume-) nullify a note belonging to A’, without learning that A’ relates to A.

These requirements feel like the fall under the above category of:
“A wants to interact with B, without B learning who A is: A creates a stealth address A’.”

Or maybe it’s slightly different:

A wants to enable B to privately-spend A’s notes, without B learning A: A creates a stealth address A’.

It’s certainly different from the “main” use case: A wants to call a public function: A creates a stealth address A’..

Depends on what we define as " A creates a stealth address A’ ", if there is some off-channel communication, then yes, A can share A’ to whoever that wants to interact with it, avoiding A’, A’‘, A’‘’ from knowing that they’re interacting with the same address.

If there’s no off-channel communication, then A will probably interact with some public registry, so that B can find its address to, for example, settle a buy-order. In this sense, what’s happening is that A is interacting with a smart contract, and using the priv → pub method with a stealthy mechanism.