Always producing an additional nullifier used as a tx hash

It is being considered that in order to have a tx hash reliably produced on the client side we would always inject an additional nullifer as a first nullifier of a tx and this nullifier would be used as tx hash (tx id).

We discussed in a call that this nullifier would be injected even for txs which produce “real” nullifiers because a “real” nullifier would not capture the effect of the whole tx. I don’t fully follow why that is an issue given that the tx hash is only used to identify a tx (am I mistaken here?). Is this really an issue?


The way I see it, for a tx hash to properly identify a tx, it means that two different txs cannot have the same hash. If all a tx can do is insert and consume private state (ie insert private data commitments and nullifiers), then it makes sense that the hash of the set of nullifiers and data commitments uniquely identifies it (provided those are unique as well).

But now a tx can also call a public function. This means that a tx can also affect the public state tree. So the changes to the nullifier and private data trees are no longer enough for identifying it. And including the changes to the public state tree in the tx hash do not work, because those are not known until the tx has been processed by the sequencer, and you need to be able to identify a tx since the moment the client assembles it.

So @Mike suggested going with the same model that Ethereum has, and just use the hash of the tx intent (ie tx request, the from/to/calldata/etc) as the identifier, which is a great idea. The new problem is that, if you’re an archiver, you can’t reconstruct the tx hash from the data that gets published on-chain (since it only has changes to the world state trees).

So the alternative is either including the full tx request in the chain (but this is costly, and it breaks privacy), or including the tx hash itself, and having a circuit verify that the tx hash is correct wrt the tx request. We considered including it as a separate field, but Mike suggested including it as a nullifier, since we get for free the check that the same tx is not included twice.

The changes for doing this are captured in this issue.


Understand. So the core of the issue is that if we didn’t inject the nullifier and used a “real” one there could be 2 transactions produced which would nullify the same note and hence have the same tx id even though their effects could be totally different.

Are there some other scenarios where this is an issue or is only the case of user producing 2 “competing” txs on the client side and not knowing which is which?

1 Like

Sorry, I missed the reply notification! I’m not sure if it’s possible for two users to consume the same nullifier, in that case, the problem would be extended across more than a single user.

I don’t think there is a problem with two users producing conflicting nullifiers. The users’s private key is one of the hash inputs used to generate the nullifier (prototype simulator code here and example nullifier struct here).

You could however have a situation where the same user submits two transactions, each of which creates the same nullifier. Only one can be included in a block of course because the second one would fail non-membership check for that nullifier.

But regardless, if a user basically just resends their exact same transaction (with the same nullifier & commitment set), the TX identifier/hash would be identical (using the first method @spalladino mentions). But I don’t think this is really an issue because it would just be dropped and ignored as an obvious exact duplicate TX.

Correction: two users could create the same nullifier.

It sounds like our nullifier code that I pointed to is just an example. The [current] goal is to have “custom nullifiers” where an app circuit is responsible for defining the structure of the nullifer and the rules for who can nullify what. So, the user’s private key may not be a part of the nullifier. That is up to the app circuit.

Nullifiers will be hashed again with contract_address in the private kernel to ensure siloing between contracts (to prevent nullifier collisions between contracts).

Edit: so I guess that is relevant here and supports our usage of a TX hash and not just hash of nullifiers & commitments. I think the main issues Santiago mentions are the ability of nodes to update world state and identify which TXs got included in a block. I am still making sense of those issues and our decision.

Edit: relevant convo on custom nullifiers here

1 Like