The next part is analyzing each input and the possibility to forge, replay and abuse the zero knowledge property for each input:
rootThis is the Merkle root of the tree, implemented in this circuit. We cannot forge this root according to
this require(isKnownRoot(_root), "Cannot find your merkle root");. The reuse of the same root does make sense only with another nullifier, so there is no problem here. The root is public that's why we don't have a ZK problem here. Passed.
nullifierHashThis is the Pedersen hash of
nullifier + secret. We have already discussed Pedersen hashing above, and, since the
nullifierHash and secret
nullifier and
secret inputs are tied and checked by the circuit, we can say it's safe against the forgeability of the
nullifierHash. The replayability issues are mitigated by the
require(!nullifierHashes[_nullifierHash], "..."); check in the contract
here. The zero-knowledge property here is a little tricky because the Pedersen hash doesn't generate fully pseudorandom values and in some cases some suggestions about its preimage can be made, but in our case the secure generation of
secret and
nullifier mitigates this issue. Passed.
recipient, relayer, fee, refundThese values are external parameters used mostly by the contracts. We have already checked that they are included in the proof, so they cannot be replaced by other values using the same proof. Their replay is possible simply by design and they are public, so no zero-knowledge compromise is present. Passed.
nullifier + secretForging these parameters, allowing the same deposit to be reused twice or more, represents the main attack vector against Tornado. The goal of an attacker here is to submit a secondary
nullifierHash "derived" from the single deposit (a single pair of
secret and
nullifier). If the Pedersen hash is checked in the contract, using a different Pedersen implementation (other than the one used in Circom), there could be some issues when the same values lead to different hashes in two implementations, but here the commitment hashing is checked in the circuit, making this attack vector inapplicable. Another forge/replay attack vector is possible when the
secret + nullifier encoding somehow uses different bit lengths, allowing situations like
(secre[t] + nullifier) == (secre + [t]nullifier), but here this situation is also not applicable. Zero-knowledge abuse cannot be exploited here as well due to the use of Pedersen hashing with enough bits of security and a suitably secure elliptic curve. Passed.
pathElements[levels] + pathIndices[levels]These values are the Merkle proof of the commitment inclusion. The Merkle tree topics have been discussed above. The amount of
levels is a circuit template parameter, so we cannot play with the size of the Merkle proof and secondary preimage attacks. Now, zero-knowledge property: in case when a single, "first in the tree" deposit was performed and a single withdrawal has followed, we, of course, can determine which leaf was used and deanonymize the depositor. Tornado works only when multiple deposits from different users were made. So, it's not possible to deploy "my own Tornado pool for myself", it will face this problem. The anonymity here is provided by many independent users, and when developing solutions based on Tornado scheme, you should always consider this issue. This is a good example of how the security of an underlying ZK protocol can become nonsense if the protocol is used incorrectly.