Who is msg.sender when making private -> public function calls

High level problem

  1. Having msg.sender be a private contract address damages anonymity

  2. Allowing msg.sender to be obscured makes writing public functions unintuitive from the perspective of an Ethereum developer

What we desire

When a user is interacting with a public protocol, they should be able to seamlessly perform the following pattern or obtain anonymity benefits equivalent to this pattern

  1. Unshield tokens into a random single-use address
  2. Perform a defi interaction via the single-use address
  3. Take the proceeds of the defi interaction and shield them into their main address

Possible solution

  1. Public function calls have an additional boolean status flag from_shielded_address
  2. For public->public calls, from_shielded_address = true
  3. For private->public calls, from_shielded_address value defined by the private function making the call

If from_shielded_address == true, `msg.sender = -1

Pros:

  • Maximum anonymity

Cons:

  • Hard to write public functions using this paradigm
  • Does not solve the DevEx issue of having to create single-use accounts/addresses for DeFi interactions that require persistent state that is linked to the user (e.g. a collateralised debt position or a share in a liquidity pool)
39 Likes

I’m just trying to consider how one could achieve this with the current protocol:

Say we have a public function set_my_mapping (for example’s sake). A dev could include some call_as_anon function in their contract:

global my_mapping: Mapping<Field, Field>;

fn set_my_mapping(a: Field, anon_address: Field) {
    require(msg_sender == address(this)); // solidity, but you get the idea
    my_mapping[anon_address] = a;
}

// The syntax doesn't quite work, here, but it's illustrative.
secret fn call_as_anon<N: Field>(function_selector: Field, args: [Field; N] randomness: Field) {
        let anon_address = hash(msg_sender, randomness);
        args.push(anon_address);
        call(address(this), function_selector, args);
}

Thoughts on whether this is bad UX?
Not sure on which is the “better” approach out of this and your suggestion, yet, but thought I’d throw this together.

28 Likes

I like the proposed approach by @zac-williamson – especially over creating random single use addresses at the contract level.

If a dApp or wallet developer has to manage multiple addresses, the wallet UX will be poor → unusable. You will have to show your assets split between your public and private accounts, or attempt to aggregate them :frowning:

The protocol could have a deterministic way to generate those addresses, or someone (us?) creates a very strong standard, however that seems like a bit of a minefield.

It could be worth removing the message sender all together for private > public transactions. A big move away from Solidity, and I am sure will have other negative consequences but some rational for discusion.

Let’s take two versions of a DEX that let’s a user add liquidity.

DEX A– Records positions in a public mapping msg.sender > position
DEX B – Records the pool total in public state, but positions in the private state tree

IMO, end users will pick between two protocols as follows:

  1. I am using Aztec I have privacy.
  2. Protocol A is 1 tx, vs protocol B which is 2.
  3. Pick protocol A.

Some time later… all liquidity will be on protocol A as many users pick A.

If some liquidity still exists on B, B users will have a higher cost to using the private version (from multiple txs or slippage).


Dev Ex

The contract developer devex without msg.sender in the public world is actuably reasonable thanks to Note abstractions.

Note this relies heavily on a custom note implementation that works like AC claim notes.


// ignore the horrific rust / solidity mashup.

import privateToken from './token'
import note from './note'

contract {
  // UTXO set mapping for storing private notes
  Option(field: Option(field: note::UTXOSet)): privateBalances = [][];
	
  private function privateLP(Field: amount, Field: assetIdTo, Field: assetIdFrom, Field:
    privateOwner) {
			
    contract privateToken::at(assetIdTo);
    // send this contract a specific note (imagine this exists)
    note::UTXO:note1 = privateToken::transferFromNote(
    amount); 
    // contract owns the note, it can nullify
    note1::remove();
    // create a peddersen encryption of the owner
    field privateOwner = note::peddersenEncrypt(msg.sender);
    // call a public function
    public this::addLP(note1.value, assetIdTo, assetIdFrom, privateOwner)
  }

  public function addLP(Field: amount, Field: assetIdTo, Field: assetIdFrom, Field:
    privateOwner) {
	  
   // get the price ( we can do this as we are in the public land)

   (to, from) = this::computeAtoB(amount, assetIdTo, assetIdfrom);

    // missing checks for slippage

    poolTotals[assetIdTo] = poolTotals[assetIdTo] + to;
    poolTotals[assetIdFrom] = poolTotals[assetIdFrom] + from;

    // always return any state for the user to the data tree, likely using peddesen homomorphic hashing magic.
    // this would be decided by the note implementation used

    privateBalances[assetIdTo][assetIdFrom]::insert(note::completeEncryption(privateOwner, amount));

  }
}


TLDR

UTXO’s are the perfect throwaway addresses IMO.

31 Likes

Tbh, this has actually been my mental model for how we’d do anonymous public state - copy aztec connect’s approach. We can likely tidy up the syntax.

But we don’t need to get rid of msg.sender to make your example work, Joe. The public function in your example just chooses not to access msg.sender, and so no protocol changes are needed.

I like the approach we’ve been taking of late*; of removing complexity from the protocol, and instead giving users the flexibility to solve problems in their own way, even if it results in more verbose contracts. Keeping msg.sender – but pushing the problem of ensuring anonymity of users to devs – feels like another opportunity to keep the protocol simpler.

Re your DEX A & B example, aren’t they both actually 1 tx, since both public and private function calls go via the private kernel circuit now?

We could add a not-anonymous decorator to function declarations, for the Noir compiler to ensure msg.sender can only be accessed by public functions which are decorated with this keyword, and to alert users (albeit only advanced users who actually read contracts) that the function reads msg.sender?

It could be worth removing the message sender all together for private > public transactions.

Not sure how we’d be able to achieve this. A public function can always be called by a public function, in which case it might want to be able to read msg.sender. A public function isn’t aware of what kind of function it’s called by, so can’t conditionally choose to ignore a msg.sender lookup. But if the other things I say earlier in this message ring true, then no need to do this.

*E.g. simplification of the L1<>L2 messaging spec; and error handling of messages; and syntax for utxos.

(P.S. My example code above was perhaps too misguided by this afternoon’s phone call. Using notes for anonymity has been my preference for this problem, too. Although my example is an approach a dev could take, if they were happy with the pain of tracking ephemeral addresses in their dapp. Point being: it’s up to the dev).

I agree with @jaosef in thinking that not having msg.sender is probably a good idea. We are different from Ethereum and I think in this case we should embrace this.

The following is the reasoning for why I think this. When I was implementing Liquity Trove bridge, msg.sender became problematic because Liquity uses msg.sender to determine who owns the Trove and there can be only 1 Trove per msg.sender. This resulted in us needing to have 1 bridge per Trove and this was detrimental to UX. This could be avoided if Liquity instead minted an NFT representing the position (this is what Uniswap is doing when providing liquidity).

I think the Liquity case is showing well that msg.sender can be easily replaced with an NFT/note. I would not expect devs to have an issue with us not having msg.sender if we manage to communicate this difference well. To communicate this I would use the concept of NFTs because smart contract devs already know them well.

38 Likes

Keeping msg.sender – but pushing the problem of ensuring anonymity of users to devs – feels like another opportunity to keep the protocol simpler.

@Mike I would expect devs to not manage to do this well and I think we should aim to design the protocol in such a way that not ruining privacy is the default.

1 Like

Thanks Jan and sorry for the delayed response.

@Mike maybe a path forward is not call it msg.sender and have an context variable that denotes a parent function caller. Given the differences with Ethereum and its EOA’s I think a name without prior meeting would lead to less confusion.

This model would work similar to zac’s suggestions, the context would have a variable context.caller that would be set as:

Private → Public (DEFAULT = unset. Optionally set, if set the value would be a private contract address, likely the users AA contract address if they are just calling a private function)
Public → Public (set, and would be prior public contract address)

I feel like this would avoid confusions and footgun’s for developers by having the protocol by default anonymous. It would also avoid antipatterns that @benesjan mentioned and hopefully prompt devs to adopt the NTF model for positions.

I worry that msg.sender (or indeed caller, if renamed) wouldn’t be able to be written in any public function under this paradigm, because if the public function were ever to be called by a private function (which can’t be ruled-out), msg.sender would be 0, so the function would ‘break’.

I think keeping msg.sender can still be made to work.

I agree, the NFT paradigm is a nice one.

But… if a dev chooses to write a public function that uses msg.sender (and a lot of devs will), then a private ‘wrapper’ function could serve as msg.sender for all calls made to this function from the private world. The ‘wrapper’ function could create private notes to keep track of information for individual, anonymous users, who don’t want to reveal msg.sender.

This allows devs to copy-paste (ish) the logic of any Ethereum contract to our public L2, which I think is powerful.


E.g.

contract A {
    // mapping from users to state
    my_public_mapping: Mapping<Field, Field>;
    
    public fn increase(a: Field) {
        my_public_mapping[msg.sender] += a;
    }

    public fn decrease(a: Field) {
    {
        my_public_mapping[msg.sender] -= a;
    }
}
contract WrapperOfA {

    struct Note {
        value: Field,
        owner: Field,
        randomness: Field
    }
    
    secret my_private_mapping: Mapping<Field, Singleton<Note>>;
    
    secret fn increase(a: Field) {

        // track the actual msg.sender, with a private note:
        my_private_mapping[msg.sender].insert(Note::new({
            value: a,
            owner: msg.sender,
            randomness: rand()
        });

        // to the public world, msg.sender will be contract B
        A::increase(a);
    }

     secret fn decrease(a: Field) {

        // track the actual msg.sender, with a private note:
        let note: Note = my_private_mapping[msg.sender].get_1(); // unconstrained filtering
                                                                 // function omitted, for brevity
        assert(note.value == a); // simplified
        note.remove();

        // to the public world, msg.sender will be contract B
        A::decrease(a);
    }
}