In March 2021, we launched Aztec 2.0, which enables users to shield and send funds privately through Aztec private rollups. Aztec 2.0 utilizes our state of the art zkSNARK proving system, PLONK, developed in-house for the express purpose of scaling Ethereum with strong user privacy guarantees.
Aztec 2.0 is built with bleeding-edge cryptography and it is critical to promptly address any bugs. Aztec is in a continual state of audit internally and externally, incentivized by a bug bounty with Immunefi. Our core team discovered two security vulnerabilities as part of our internal efforts, with special thanks to our chief scientist Ariel Gabizon. Community members Sean Bowe and Daira Hopwood also highlighted vulnerabilities.
Transparency is important to us. We want our users to trust our technology not because nobody can understand it, but because anybody can understand it. In this post, we discuss the bugs discovered in Aztec 2.0 after deployment. These security vulnerabilities have been patched and we are confident no user funds have been lost.
Bugs found and addressed pre-launch
Bug: Pedersen hash input checks
We use Pedersen hashes inside our circuits when a collision-resistant hash function is required (i.e. when the hash function does not need to be modelled as a random oracle).
When performing a Pedersen hash in TurboPlonk, the binary representation of each input field element is split into 128 2-bit windows, whose sum is equal to the input.
Each window is used to index a 2-bit lookup table of elliptic curve generator points, which are summed together to produce the Pedersen hash output.
The bug was that, when validating the sum of the windows equalled the input field element, we were validating this [mod p], where p is the native circuit modulus.
This meant that every hash input effectively had two possible representations in 2-bit window form (the actual binary value or the value + [p]). This meant that every Pedersen hash effectively had two different outputs.
A consequence of this bug is that it was possible to generate two nullifiers for every note. This would enable a double-spending attack.
Our circuits were updated to always validate the sum of Pedersen hash input 2-bit windows were [< p], when required.
Risks to Users
None. This bug was identified and fixed before we launched.
Bugs found and patched post-launch
Bug: Merkle root position check
The rollup contains a “root” tree; a Merkle tree containing the past Merkle roots of the note tree (which contains all join-split “value” notes and user “account” notes).
As part of the root rollup circuit, the rollup provider must compute the new root of the note tree and insert it into the root tree. The intended position of the new leaf in the tree is directly adjacent to the rightmost non-zero leaf; i.e. the tree is initialized to all zero leaves, and then updated from left-to-right.
The bug was that our circuit did not actually constrain the position of the new leaf. In reality, the rollup provider could have inserted the new leaf at *any* position in the root tree. An adversary would have been able to insert a leaf at an arbitrary location in the root tree and not reveal the location (this location is not a public input).
If after such an insertion the adversary doesn’t participate in future rollup creation, from that point on *nobody else* can create a valid rollup and the system is frozen and unable to process for any future transactions.
The root rollup circuit was modified to validate leaves inserted into the root tree are at the correct position. The rollup smart contract was updated to use the verification keys from the new circuit.
Risks to users
In theory, a malicious actor had two months to find and exploit this bug. However, the only entity able to launch this attack was the rollup provider. Currently, only Aztec can create and submit rollup proofs.
We confirmed there was no attack by reconstructing the data root tree and validating there are no out-of-position leaves.
Bug: Recursive proof verification
When aggregating private transactions in our rollup circuit, we use the following circuit structure:
Join-split Circuit: Executes a private transaction; generated by the user locally on their device.
Rollup Circuit: Verifies the correctness of 28 join-split circuit proofs and performs database updates into the rollup’s Merkle trees.
Root Rollup Circuit: Verifies the correctness of 4 rollup circuit proofs.
When verifying a Plonk proof inside one of our circuits, partial verification and proof aggregation occurs.
Each proof is verified up to the point that a bilinear pairing check is required. The Plonk verification algorithm’s bilinear pairing check is structured such that both G2 group elements are fixed and do not vary between different proofs.
Instead of performing this pairing check inside our root rollup circuit, these two group elements are defined to be public inputs of the root rollup circuit. i.e. they are broadcasted on-chain as part of the root rollup proof.
The verifier smart contract will then extract the two group elements and aggregate them into the pairing check computed by the smart contract.
The bug was that, when performing the proof aggregation step in the root rollup circuit, we were aggregating only the rollup proofs, but not the join-split proofs.
The root rollup circuit was modified to correctly aggregate join-split circuit proofs. The rollup smart contract was updated to use the verification keys from the new circuit.
Risks to Users
This bug enables an adversary to generate fake join-split proofs (e.g. double spend transactions). This would not have been picked up by either the rollup circuit logic or the verifier smart contract logic. If an attacker generated a fake join-split proof, they would need to convince a rollup provider to include their malicious proof in a rollup circuit proof.
Aztec is currently the only rollup provider and we use our falafel software library to validate the correctness of every join-split proof included in a rollup. The verification logic in falafel was not affected by this bug. As a result, we are confident that no malicious proofs were included in any rollup block because of this bug
Bug: Generating randomness
When generating random secrets, a Mersenne Twister was being used with a random seed. The determinism of the twister made this unsuitable as all random variables produced in a proof could be determined with knowledge of one of them. This issue should not have affected the generation of user secrets and private keys.
The Mersenne Twister was removed. Random number generation is delegated to the base operating system. On the web, this is done via the WebCrypto API.
Risks to Users
This bug affected random numbers generated in two instances:
- Users creating privacy proofs
- Rollup providers creating rollup proofs
The rollup proof does not have to be zero-knowledge as no secret information is hidden (privacy is achieved entirely via the privacy proof).
When constructing a privacy proof, several random variables are generated as blinding factors. If any of these leak, then it is possible to recover the remaining randomness using this bug and remove the blinding factors from the proof. In theory, this would allow an attacker to recover user secrets and private keys.
It is expected that no random variables are leaked when generating a proof. If a variable is leaked, the user’s device is compromised and it is likely an attacker has access to all 12 random variables regardless.
Bug: Generating prime field elements
When generating random 254-bit prime field elements, a random 256-bit number was generated and then truncated modulo the field order. This produces random numbers where smaller values have a significant positive bias.
Risks to Users
Notes and nullifiers generated prior to May 6th will be marginally easier to decipher via brute-force attacks. Such attacks are still not remotely practical and we are confident that users affected by this issue do not need to regenerate their Aztec private keys.
Random field elements are now generated via creating a 512-bit number and reducing modulo the field modulus. This largely eliminates any bias in the resulting field element.
Bug: Not checking decrypted note details match insertion to tree
During a transaction, a recipient receives the details of their new note in an encrypted message. We weren’t checking that the commitment added to the note tree indeed corresponds to these note details.
Risks to Users
An attacker could have made the user think they received funds that were not really sent. Only when trying to spend the funds using the decrypted note details, would the user realize the problem.
In our code update from May 6th, we added the required checks in our client software that validates viewing keys map to legitimate notes in the Aztec state tree.
We developed PLONK in order to bring scalable privacy to Ethereum. As the team behind this breakthrough cryptography, we take our responsibility to the security of users’ funds and user privacy very seriously. Particularly during these early deployment stages, we will continuously audit and patch the code as necessary.
While we have an incredibly talented core team, we don’t expect that potential vulnerabilities will solely be detected internally. Community members are an essential part of our development process. We welcome your feedback and contributions to our auditing efforts. Get in touch with our team on Discord. Our currently deployed code can be found on our bug bounty repository.