Sunspot Security Audit Case Study

Alex
Cryptography Engineer
Between April and May 2026, HashCloak audited Sunspot, a toolchain that lets developers prove and verify Noir circuits on Solana. Over the course of the engagement we reported 26 findings, including 2 Critical and 12 High severity issues. Every one of them has since been resolved by the Reilabs team.
Since Sunspot can be viewed as a proving backend that act as a translation layer between Noir and Gnark, we focused on whether the Sunspot compiler can fully capture the meaning of a Noir circuit. During the audit, we found several issues arise from the subtle mismatches between the Noir and Gnark circuit. On it's own the translated Gnark circuit seems correct, but the constraint is slightly different from how Noir is defined or implemented. In zero-knowledge proof systems, this "slightly different" in circuit definition can sometimes lead to a big impact on the safety of a real world proving system as we will see in some issues later.
What Sunspot Actually Does
Noir is a popular language for writing zero-knowledge circuits, and it usually proves and verifies through the Barretenberg backend. Sunspot offers an alternative path: it takes a compiled Noir circuit (ACIR) and translates it into a Gnark circuit, generates a proof using an enhanced version of Groth16, and then auto-generates a Solana verifier program through the gnark-solana library. (We go deeper on how Noir compiles and optimizes its circuit in another post, see the Further Reading section.)

Pic: Full end-to-end flow for Sunspot
The result is appealing. A developer writes a circuit once in Noir, besides the solidity verifier comes with Noir, Sunspot makes it verifiable cheaply and natively on Solana. The verification should pass if and only if the witness satisfies the original Noir circuit.
The last sentence also capture the security model. The safety of Sunspot relies on the Gnark circuit it build is semantically equivalent to the Noir circuit the developer wrote. If there are differences between the two, it can violate the fundamental security gurantees of the ZK protocol. The following possible issues may arise:
A correctness problem -- honest provers can't generate proofs for circuit on Solana that should be valid
A soundness problem -- malicious provers can generate proofs that should be invalid but pass the Solana verifier
Zero-knowledge issue -- leakage of sensitive inputs/witness information beyond statement validity.
Most of the high-impact findings in this audit were soundness problems hiding in the gap between "what Noir means" and "what the generated Gnark circuit enforces"
Audit Scope and Methodology
The review covered the Sunspot repository at commit: 3742263b4bd2be13a61f17d146c7545527529e55
The assessment included:
Noir ACIR translation
Black-box function implementations
Recursive proof verification
Witness management
Grumpkin elliptic curve arithmetic
BN254 operations
Gnark-solana integration
The audit was conducted through a combination of thread modeling and manual code review. Based on the security requirements of Sunspot, we developed several security properties that each component must follow, especially for the correctness and soundness when translating a Noir circuit to Gnark. During the manual review phase, we first developed a model of how Noir's own backend handles each operation, derived a set of equivalence properties the Gnark output had to satisfy, and then went looking for places where Sunspot's implementation violate them. We then systematically assessed the codebase against identified areas of concern and potential attack surfaces during the thread modeling.
Interesting Findings Worth Dwelling on
Underconstrained Blake2s Hash Output (SNT-1, Critical)
One of the most common classes of bugs in zero-knowledge circuit implementations is the underconstrained circuits. A situation where a value is computed correctly inside the circuit but never actually tied to a constraint that the verifier checks. Unlike a traditional program bug where incorrect output is observable, an underconstrained circuit is silent: the prover's witness generation fills in the correct value, all tests pass, and the circuit appears to work perfectly. The problem only manifests when a malicious prover deliberately supplies an incorrect value, and the verifier happily accepts it because no constraint ever said it had to be anything in particular.
Since, Blake2s is a 256-bit hash function i.e., it produces a 32-byte digest, we expect the entire 32-byte output to be constrained. In Sunspot, the Blake2s output array has 32 entries (one per output byte), all registered in FillWitnessTree so the prover correctly fills them during witness generation but the issue was found to be in Define function. The code calls Blake2Permute, which runs the full Blake2s compression correctly and produces an internal state h of 8 32-bit words. However, Blake2Permute returns only h[0:4] i.e., the first four words and the assertion loop that follows iterates for i := range 4, constraining only Outputs[0] through Outputs[15]. The last four state words, h[4:8], are simply never returned, and Outputs[16] through Outputs[31] are never constrainted.
The practical consequence is that a prover could set the last 16 bytes of a Blake2s output to whatever they wanted and still produce a valid proof. Any Noir program that uses the full hash output was relying on a value that could be set to anything.
The fix was small and the Sunspot team has already mitigated the issue by looping over all 32 bytes. This issue is an example that underconstrained output can sometimes be hard to spot since the circuit compiles, and the honest-prover tests pass. However the consequences can be critical if the prover can freely set the values.
Unchecked recursive proof KeyHash (SNT-8, High)
In recursive aggregation, the KeyHash is supposed to bind a proof to the verification key it was made against. In Sunspot it was an entirely free witness meaning the prover could set it to any value.
The issue opens two doors. A prover could generate multiple valid proofs that differ only in their KeyHash. Worse, if a downstream circuit checks the KeyHash instead of the full verification key, which is the intended usage of theKeyHash, an attacker could pair an incorrect verification key with a correct KeyHash and pass the verification.
In a zero-knowledge proof system, any unconstrained free witness can lead to some security issues. A slightly different but similar example is to add a "dummy square" in the circom circuit to prevent malleability, see this blog post for Semaphore for more info. Having this free-witness issue in mind while auditing is helpful since this is a commonly overlooked issue both during development and testing given that an unused witness doesn't impact the system most of the time. This is also how we were able to locate this issue during the audit.
This is an example on how a subtle design difference between proving backends of Noir can impact safety of the proving system. In Noir, the KeyHash won't be checked in the language frontend (The compilation from Noir to ACIR), but will be deferred to the backends' implementation. That means the backend can choose it's own hash functions to verify the KeyHash, and it's legal if the backend decides not to use the KeyHash input entirely. However, the KeyHash is still part of the interface of the Noir Recursive Proofs black box function and hence will be included in the witness. The fix from the Sunspot team is to constrain the KeyHash in the witness to always be zero. This way, it forces the developer to not rely on the KeyHash and also prevent same proofs with different KeyHash to be valid.
The point validation issues (SNT-3, SNT-4, SNT-5, High)
A cluster of findings came from the same source: untrusted elliptic curve inputs that didn't verify the point validity.
embedded_curve_addandmulti_scalar_mulfunctions are using Grumpkin points coming from unchecked witnesses.The public keys of
ecdsa_secp256k1andecdsa_secp256r1are unverified witness inputs.The Groth16 verification keys and proof for
recursive_aggregationare unverified BN254 G1/G2 points.
Missing point validation is a common issue when implementing elliptic curve operations. We have seen similar issue occurs multiple times during our past audits. For example in our latest PSE MACI audit, there's a similar issue where a public key witness input has not been verified in the circom circuit. It is important to view every prover provided data as unsafe since any invalid data may compromise the entire proving system. Further more, for an honest prover feeding in random valid points none of the issues will be triggered, so it's common to be omitted during testing. A malicious prover could choose invalid points that will pass the verification and break the proving system.
Mismatch behavior of ECDSA signature verification between Noir and Sunspot (SNT-7, High)
Another finding stemming from the difference between the implementation of Noir and Gnark circuit in Sunspot. As mentioned in the methodology part, throughout the entire audit process we keep the model of how Noir handles each operation in mind. Even if the implementation in Sunspot looks correct on it's own, we still compare the implementation details to the Noir counterpart. As it turns out, several subtle differences was found. This issue is particularly important since signature normalization is specifically mentioned in the Noir spec. Thus circuit developers will expect the same requirement holds throughout different proving backends.
In the Noir documentation, it requires the s in the signature to be normalized (signature (r, s) with s > order/2 is not allowed). However, in the Gnark ECDSA library, it only checks that r and s are smaller than the modulus. Since this condition is required in Noir, users might assume the same condition holds in Sunspot and could lead to a malleability attack where an attacker can derive (r, -s) from a signature (r, s) and pass the verification.
Witness counting issues for sub-circuit in Noir (SNT-9, SNT-10, High)
Two similar issues related to counting witnesses in a sub-circuit. Sub-circuit (#[fold] in Noir) is meant to be use in the future Incrementally Verifiable Computation (IVC) support for Noir. It is not currently supported in the Barretenberg backend but interpreted as a function call.
SNT-9: Unused witness in the sub-circuit will be omitted when counting the total number of witnesses for the sub-circuits, result in error during proving. The original approach of counting the witness is by counting the inputs and outputs of each opcode. However, if there's an unused witness the number will be incorrect.
SNT-10: Public witness in the sub-circuit will be skipped in the input list, causing the compiling process to fail.
Since the #[fold] keyword is rarely used in Noir, we only found the issue after we closely examine the code line by line. This example shows the intricate nature of a proving system, especially the backend or compiler of the circuit, and it requires careful inspection during audit.
Closing Thoughts
Noir is a versatile ZK language that can support proving backends with different zero-knowledge proving schemes (see here for a list of proving backends). When implementing a new proving backend, it is important to faithfully reproduce Noir's semantics. As the example findings have shown above, the mismatch behavior between different proving backends might break the proving scheme in practice.
Some other common traps when developing zero-knowledge proof systems includes underconstraint of witness (SNT-1, SNT-8) and unvalidated witness input (SNT-3, SNT-4, SNT-5) which can both lead to some serious attacks in practice. When dealing with prover-supplied witness inputs, it is important to verify they follow all the requirements of the proving system.
A complete and through test that covers not only the happy path but every edge case can prevent a lot of issues mentioned above during the development phase.
Further Reading
In LINK TO Noir Circuit Optimizations Write-up, we dig into how Noir compiles the circuit and applies optimization for the ACIR (Abstract Circuit Intermediate Representation). The optimization shares similar concept to the translation layer here since both require the compiler to faithfully preserve the semantic of the original circuit.
