The o1js Audit: Trust but Verify

At o1Labs, security is foundational to everything we build. In collaboration with Veridise, a leading blockchain security firm, we conducted an in-depth audit of o1js, ensuring that critical vulnerabilities were identified and resolved ahead of our v2.0 release.

image

At o1Labs, we recognize that security is not just a priority—it's a fundamental responsibility. Ensuring the integrity of the tools we build is paramount, which is why audits and code reviews are integral parts of our security strategy. Our belief in the necessity of rigorous, transparent, and comprehensive security practices led us to partner with Veridise for a third-party security audit of o1js.

Why We Audit

Our philosophy for this audit was simple: be proactive, not reactive. Security audits provide an external perspective, validating our internal processes and strengthening the resilience of our stack. In addition to audits, we enforce a strict code review policy—nothing is merged without a second engineer’s review. We also invest heavily in testing, using property-based tests on a broad distribution of random samples. This approach ensures excellent coverage of correctness, such as verifying the equivalence of in-circuit and out-of-circuit implementations. By embracing third-party audits and rigorous internal practices, we position ourselves as responsible leaders committed to delivering secure and reliable products.

The Veridise Audit: A Testament to Our Security Commitment

Prior to releasing v1.0 of o1js, we conducted an internal audit. However, we knew that to truly guarantee the security of our product, we required an external review, so we began our partner selection process.

We chose to engage Veridise, a leading blockchain security firm, in a comprehensive audit because they were familiar with the o1js codebase and have an excellent reputation, which is validated by their previous audit of the MinaPortal MetaMask Snap and our positive interactions with their team in the past. Veridise conducted this audit over 39 person-weeks, with 2 security analysts reviewing code over 13 weeks, and 2 additional analysts providing further reviews and engineering effort to aid with critical cryptographic regions of the code and to increase the level of automated testing.

This audit was thorough and meticulous, covering every aspect of o1js: circuits, protocol code, cryptographic primitives and more. Veridise identified 17 issues in the critical, high or medium categories, and we are pleased to report that all of those have been addressed by our team - ensuring that everything with significant impact or probability was resolved. The audit also acknowledged several lower-severity issues, but these were deemed to be non-critical. In addition to finding issues by manual audit, Veridise performed formal analysis and fuzzing which confirmed the correctness of some mathematically complex pieces of code, and even helped uncover an issue here and there.

For those interested in the specifics of the findings and our responses, a detailed report outlining the vulnerabilities identified and the measures we have taken to address them can be found here.

Note to developers: Since many of the fixes required breaking changes, the final version of o1js containing all fixes will be a major release, v2.0. In the meantime, vulnerable methods have been deprecated and safe variants suffixed with `v2` have been added.

A critical issue caught early: Underconstrained Merkle maps

To give an example of Veridise’s work, let us describe one of the two critical issues in detail. o1js features a “Merkle map” data type which implements a zk-friendly key-value store using a sparse Merkle tree. The sparsity allows us to make the tree 2^255 leaves wide, so that every possible field element can serve as the index of a leaf. A key-value pair is stored at the index deterministically derived as the Poseidon hash of the key (which is an arbitrary field element). This is not only convenient – you don’t have to keep track of where an entry is stored – but also enables non-inclusion proofs: To show non-inclusion of a key, you provide a Merkle path to an “empty” value stored at hash(key).

However, our design had a flaw: Merkle paths represent the index they point to as 255 bit values, which are accumulated to a field element to prove that it equals the target index. Since our circuit field’s size is just slightly larger than 2^254, the decomposition of an index into 255 bits is not unique: There are two possible decompositions for most field elements, and so two different Merkle paths appear as valid for a single key! By picking the “other” Merkle path, a malicious prover could, for example, show non-inclusion of a value that is actually included.

This is a highly critical issue that would, for example, allow double-spends if the Merkle map was used to store nullifiers. It was caused by a classic circuit-writing mistake: forgetting to account for field overflow. We applaud Veridise for being extremely reliable in catching mistakes like this, including more subtle issues than the one just described.

Looking Ahead: Continued Vigilance in Security

Our collaboration with Veridise is part of our ongoing commitment to maintaining and enhancing the security of our technology. Security is not a destination but a journey—one that requires continuous vigilance and dedication.

We believe that by conducting this audit we are not only protecting our developers and end users, but also contributing to the broader integrity of the ZK ecosystem. Thank you to Veridise for their exemplary work, and to our community for your trust in us. We remain committed to transparency, excellence, and above all, security.