This post is a outlines a rough overview of how to implement public → private message passing by modelling it around l1 to l2 message passing.
Right now communication between different function types in unidirectional. We can only make calls to public functions from private ones, and not the other way around.
This post seeks to outline how to transfer information from public execution environments to private ones.
In this previous document the mental model for interoperability between private and public functions can be briefly summarised as follows:
Private functions can read, edit and create private state only.
Public functions can read and edit public state, and create private state.
Private functions can initiate public function calls, but not the other way around.
We aim to devise a mechanism whereby public calls can create state that can be consumed by private transactions. However, initiating private execution from a public function call will be impossible within the protocol (though external infrastructure could provide automation triggered by events), due to the block builder executing public function calls (not users).
The key to bridging communication between public and private functions is to enable public functions to insert items into the private data tree.
The proposed solution echoes the mechanism for sending l1 to l2 messages. The preimage of these messages is entirely public (calculated on Ethereum), but private functions can consume them and nullify them without any issues.
To maintain privacy, a model similar to l1 to l2 message creation for generating nullifiers can be used:
When a commitment is created, it is formed with a secretHash.
To consume the message, the consumer must know the preimage of the secretHash.
If there is no secret component to nullifier creation, then anyone can create a nullifier for a piece of data without consuming the message, effectively rendering the commitment useless.
Public Circuit Public Inputs
Add new commitments field, similarly to how Private Circuit Public Inputs
The public kernel circuit will need to aggregate the newly created commitments, this should not change how the rollup circuits handle the commitments.
The public kernel circuits will also need to verify the sibling paths for the other calls. (Unless the public VM will be allowed to handle these inclusion proofs. It is not clear whether this will be done in the VM or kernel).
A new oracle call to access a private commitment created within a public execution.
This is required as it is another note store that is separate to the traditional store (created by trail decrypting notes).
This will be very similar to the l1 to l2 message consumption oracle calls
The archiver will need to store the preimages of these calls.
What’s the difference between the payload of an L1 → L2 message (which gets stored in the L1->L2 Messages Tree) vs the payload of a Public → Private message (which this proposal proposes to store in the private data tree)?
If we already have a Noir Contract library (and protocol infrastructure) which enables private & public functions to consume messages from L1, couldn’t we use the same library (and infrastructure) to consume ‘messages’ from public to private functions?
The only notable difference is the link l1 to l2 messages have to a portal contract, other than that they can be thought of as functionally identical.
Almost all of this infrastructure will be able to be reused which is super handy! I have a follow up post which proposes the removal of the l1 to l2 messages tree which will lessen the distinction between the two primitives even more.
How are the preimages of the public->private notes communicated to all nodes? Is it by simply emitting an ‘unencrypted log’ containing that data? And if so, how would a node identify that such an unencrypted log as a public->private call (in order to store it somewhere separate)?
Do public->private calls actually need to be stored somewhere separate from other note preimages? If they all live in the same tree, can’t the app which is capable of consuming the note interpret that note and consume it correctly? I suppose, currently, an oracle call to get a note requires a storage slot as argument. But perhaps these notes representing calls from public->private could be ascribed to storage slots?
It does not necessarily need to be communicated at all or stored separately.
It depends on the implementation of the contract. e.g., if the same user making the public → private is running the private call after he will already know the information and broadcasting it is just wasting resources.
If you want to broadcast it, you can use unencrypted log (as you mention), and the application you are using would essentially be telling you to look through events with specific topics to find it.
Might be useful to have a way to directly tell your node the pre-image of some notes without them being broadcast. I could see it being very useful to be able to load in notes that you have from elsewhere than decrypted on chain. Say you have setPreimage that the application can call, which can set the storage you have on your node to if the hash matches etc.
If you use the reveal idea from a few weeks back after dogfooding and require that broadcasting encrypted data is always explicit. You can also use the setPreimage to have contracts that don’t broadcast any pre-images on-chain but handles all that off-chain, making it very cheap to use while doing a trade-off on data availability.