In Oxide we don’t really use the secret hash when depositing to L2 (it’s set to a constant value everywhere) and that is because we want the message to contain a recipient. So what we do instead is that we have recipient_hash that is computed offchain as hash(secret, recipient). Then this recipient hash is part of the message content preimage.
(The secret in the recipient hash essentially just acts as a randomness hiding recipient from preimage attacks)
This has some nice properties:
It makes the security of messages the same as notes: “recipient” in the message is the same thing as note owner. It’s no longer only the secret that “owns” the message,
it allows someone else to claim a message on your behalf (e.g. a relayer) - but this this doesn’t seem to be that relevant anymore as oxide’s lazy claiming approach deprecates this (mentioning it here for completness).
This has some very terrible properties for Oxide because with constant secret anyone can see when a recipient spends the message (broken nullifier unlinkability property):
(Note that we could work around this by not using Aztec.nr’s process_l1_to_l2_message but I would rather refactor it all.)
Possible solutions
Solution 1: Drop the secret hash altogether and just expect the contract calling Inbox to handle it and then have standardized deposit and claim utils in Solidity and in Aztec.nr.
Solution 2: Replace secret_hash with salted_recipient_hash in the Inbox contract and everywhere and then we modify compute_l1_to_l2_message_nullifier function to use recipient’s nhk instead of the secret.
I think solution 1 is better because with the utils we can get basically equivalent devex without enshrining a specific derivation scheme in l1 contracts.
I think having a message hash and a secret hash is a nice separation.
The secret in the message is for hiding what the message does. In our case it’s the recipient. But it could also has no secret and be claimable for whoever claims it first (or with some conditions checked in the app, e.g. anyone that can prove they are over 70 years old).
And the secret in the secret hash is for hiding the fact that a nullifier has been created. An app might choose to make it constant so that anyone can see that it has been claimed.
So in oxide, maybe we just need to actually use a secret instead of a constant for the secret hash?
I think option one is better. I think the inbox is forcing a consumption pattern on portals (consume it via providing the preimage to a hashed secret, and nullify using the preimage), but the inbox should not enforce this, it should be a problem for the portal to solve (how to consume, and how to nullify is portal-specific)
Everything that @leila mentioned above can be implemented by the portal itself in an app specific manner even if we remove the secret hash from the inbox if i’m not mistaken.
Everything that @leila mentioned above can be implemented by the portal itself in an app specific manner even if we remove the secret hash from the inbox if i’m not mistaken.
Correct. Inbox doing the minimum necessary seems like a very good pattern
If we were starting again, I imagine we’d adopt the abstract approach of Option 1, where portals (and their L2 counterparts) figure out how to create a nullifier for a consumed L1->L2 msg.
Today, there are lots of example libraries (and the FeeJuice contract!) which already use the existing secretHash rails and libraries, so it would be more work [1] to update the interface, and more risk of introducing bugs into the FJ contract.
It seems there’s an Option 3:
Leave the protocol aspects of L1->L2 messaging alone, and create >=2 variants of compute_l1_to_l2_message_nullifier for different use cases, in much the same way as notes can be nullified in custom ways.
E.g. you could have both of these variants:
a) compute_l1_to_l2_message_nullifier_with_secret_hash - an alias for today’s compute_l1_to_l2_message_nullifier
b) compute_l1_to_l2_message_nullifier_with_nullifier_hiding_secret_key - which assumes, for example, the first field in the preimage of the message_hash (message_hash === the content of the L1->L2 message) represents an “owner” whose nhsk must be used in computing the nullifier.
With (b), you achieve what you want: you get a nullifier that doesn’t leak which L1->L2 msg is being consumed, and you don’t need to change the protocol. The secret_hash is ignored with variant (b).
Aside: recipient is already a term in L1->L2 message parlance which means “the contract address which contains the function that will consume this message” or equivalently “the contract address which will be ‘siloed’ with the consumption nullifier of this message”.
You’re overloading recipient (with the name recipient_hash) to also mean “the person whose nhsk will be used to compute the nullifier for the message”.
I’d suggest at least renaming your recipient_hash to something else to make this conversation (and the code) easier to follow. owner might be a good term, since that aligns with the term used for “the person who possesses the nhsk to nullify a note”.
[1] If you still want to adopt your “Option 1”, I would suggest the “definition of done” should be:
The protocol is updated, end-to-end, including the FeeJuice contract.
Prominent ecosystem libraries (incl Wonderland’s libraries) which do L1->L2 messaging “the old way” are also all updated to work with the updated protocol.
I think a simpler and better change is to simply rename content to public_content and secret to private_content, resulting in messages having public_content_hash and private_content_hash.
is nice.
From the L1 contract’s PoV it would literally be just a rename of arguments.