
Nine Minor Versions, One Soundness Hole: Auditing the SP1 Verifier Disclosure
MoonMax
Nine minor versions. That is the number that should have led the advisory, not trailed it. SP1 shipped from 6.0.0 through 6.8.0, and across that entire span, according to the disclosure, the Rust-native verification path could accept invalid proofs. Not a verifier that panics. Not a prover that fails to produce a witness. A verifier that returns true for a false statement.
I have a rule I set in late 2017, after spending forty hours inside a token distribution script and finding an integer overflow that could have drained wallets: if I cannot audit the logic, I do not trade the token. The corollary I have added since is narrower and considerably nastier. If I cannot tell you which version number closed the hole, I cannot tell you whether the system I depend on is closed. The advisory said patched. It did not say patched in what. For an operator running a relayer that pre-validates proofs in Rust before submitting them on-chain, upgrade to latest and upgrade to at least the patched minor are not the same instruction. One of them is a coin flip scheduled inside a maintenance window.
That gap, between an advisory written as crisis communication and a runbook that needs an integer, is the whole story.
SP1 is a zero-knowledge virtual machine built on the RISC-V instruction set, produced by Succinct. Developers write ordinary Rust, compile to RISC-V, and the prover emits a proof that a given program executed with a given input and produced a given output. The architecture has three surfaces that matter for this analysis: proof generation, native Rust verification, and on-chain Solidity verification using Groth16 or Plonk over elliptic-curve pairings.
The competitive set is not small. RISC Zero has the earliest production footprint and its own proof market in Boundless. Jolt, out of a16z crypto research, carries the Lasso lookup-argument lineage and a theoretical performance story. OpenVM, Pico from Brevis, Valida, and ZisK fill out the long tail with different instruction set choices and different optimization paths. What separates SP1 in practice is toolchain familiarity and integration breadth, not cryptographic novelty. Rust is the path of least resistance for developers who already write Rust. That is a distribution advantage, and distribution advantages are exactly what get tested when a security defect lands.
Two properties define a proof system. Completeness: a true statement always verifies. Soundness: a false statement never verifies. The industry talks about completeness constantly, because completeness failures are visible. Proof generation times out, the prover runs out of memory, the verifier reverts, the CI pipeline goes red, someone files an issue. Soundness failures are the opposite. They are silent by construction. A verifier that accepts an invalid proof produces no error, no log line, no failed test. It produces a green checkmark and a transaction that should not have settled.
That asymmetry is why I treat invalid proofs passing as the highest severity class in this domain, and why the phrase already patched does not reduce the severity. It reduces the urgency. Those are different variables, and conflating them is how teams end up running vulnerable code in the one place they cannot afford it.
Soundness is an order of magnitude, not a category. A completeness bug breaks availability. A soundness bug breaks the trust model. When I audited that 2017 distribution script, the overflow was interesting precisely because it did not announce itself; the math was valid on every input the developers had tested, and invalid on inputs they had not imagined. ZK soundness defects follow the same shape. They live in the gap between the set of inputs the test suite exercised and the set of inputs an adversary can construct. Nine minor versions of continuous shipping did not close that gap. That is the finding, and everything else in this piece is a consequence of it.
The claim that on-chain Solidity verification is unaffected is a claim, not a proof. Read the architecture honestly and you have two independent implementations: a Solidity verifier doing pairing checks on Groth16 or Plonk, and a Rust verifier doing whatever the native path does. Two implementations of the same mathematical object is a good design. It is also the exact configuration where a shared root cause hides most efficiently. Three places produce identical failure modes in both implementations: deserialization of the proof and verification key, binding of public inputs to the proof, and derivation of Fiat-Shamir challenges. If the defect sits in any of those three, the unaffected claim is not wrong because the Solidity code is bad. It is wrong because the boundary was drawn around the wrong component.
I cannot settle that from the disclosure, because the disclosure does not name the root cause. I mark it as an open question with medium confidence, and I mark it as an open question that a downstream integrator is not qualified to close on their own. Ledgers do not lie, only the auditors do, and an auditor who accepts a vendor's scope boundary as evidence is not auditing. He is transcribing.
The version range itself is a tell. Vulnerable from 6.0.0 through 6.8.0. An integer major version as the lower bound, followed by eight minor releases, is the signature of a regression rather than a design flaw. Design flaws tend to be present from 0.1.0 or 1.0.0 and stay present until someone rewrites the component. A defect that appears precisely at a round-number release usually arrived with a feature: a new precompile, a swapped proof-system backend, a changed serialization format, an added verification route. That matters operationally because it means anyone pinned below 6.0.0 was never exposed, and anyone who upgraded during that window inherited the exposure without knowing it existed. It also means the fix is likely small and localized, which is good news, and that the review process that approved the introducing change did not catch it, which is not.
Nine versions of dwell time is the signal nobody priced. I do not know SP1's release cadence, and I will not invent one to make a chart look authoritative. But a nine-minor stretch on a project shipping at production velocity is not a week. It is weeks to months, depending on cadence. Across that window the defect survived unit tests, integration tests, and whatever external review coverage existed. For a product whose entire commercial proposition is mathematical certainty, a test regime that did not catch a soundness regression is a more damaging disclosure than the regression. Beta is the tax you pay for ignorance, and here the tax was paid in the one currency zkVM vendors cannot print: the assumption that their tests are adversarial when in fact they are confirmatory.
Now the part the advisory did not map. Where does a Rust-native verifier actually sit in production systems? Five places, and I have seen all five deployed. Provers use it for self-checks before submitting work. Relayers and off-chain validation services use it as the final gate before an on-chain transaction. Off-chain light clients and coprocessors use it to decide whether to accept a state transition. Recursion and aggregation pipelines use it for inner-proof validation. And downstream teams fork the verifier crate and maintain their own copy, sometimes for years.
Two of those five combine into the dangerous configuration. If a protocol verifies in Rust, then wraps the result into an aggregate proof, then submits that aggregate on-chain, an invalid proof validated by the buggy path can be laundered through a legitimate outer proof. The on-chain verifier does its pairing checks correctly and approves. The statement is still false. The chain is unaffected conclusion survives only if the recursion layer never reuses the defective native verification path. Nobody outside Succinct can confirm that for a given downstream system, and most downstream teams will not think to ask, because the advisory told them the chain side was fine. Risk transferred is not risk eliminated.
The second-order damage is subtler and I have not seen it raised anywhere. If downstream projects write their test suites against SP1's Rust verifier, and that verifier accepts things it should reject, then negative tests can pass when they should fail. A team believes its circuit constraints are correct because the test asserting an invalid witness is rejected came back green. It came back green because the verifier was broken. That is a silent technical debt injection across the entire ecosystem, and it stays invisible until someone re-runs the negative tests against a patched verifier and watches them turn red. Yield without due diligence is just borrowed luck. This is borrowed correctness, and the same accounting rule applies: it gets repaid eventually, with interest, at the worst possible moment.
Downstream forks are the standing exposure surface. Open-source verifier code gets forked constantly, because teams do not want an upstream dependency sitting inside their settlement path. SP1 can patch the upstream repository in an afternoon. It cannot patch a fork sitting in a private repository with a pinned dependency that nobody has touched in four months, and it cannot force anyone to read the advisory. Fixes propagate at the speed of attention, not the speed of publication, and attention in this industry is the scarcest input on the board.
One more operational gap, and it is the one that costs people money. The advisory says the issue is patched and does not say where. For a maintainer, upgrade to latest and upgrade to at or above the fix are different risk postures. The first forces a jump across potentially untested territory during a security window. The second allows a minimal diff, a fast review, and a same-day deploy. Withholding the fix version does not improve security; it degrades the quality of every downstream remediation decision made in the following seventy-two hours. Sanity checks before sanity wins, and the first sanity check is a version integer.
The market read this correctly, and that is what worries me. Roughly 80 to 90 percent of the negative information was neutralized by the sentence chain-side Solidity verification is unaffected, which is exactly what a scope-limiting disclosure is engineered to accomplish. No funds lost. No protocol drained. No liquidation cascade. Post-hoc fix. By the standard pricing model this is a reputation event, not a liquidity event, and it should carry close to zero price impact.
Here is the contrarian read. The market will not distinguish between projects that actually depend on the defective path and projects that merely integrate SP1 somewhere in their stack. Off-chain light clients, coprocessor networks, and any architecture doing off-chain verification with on-chain execution inherit real exposure. Rollups that only touch the Solidity verifier do not. Those two cohorts will be repriced together, which means the first cohort gets marked down for a risk it genuinely carries and the second gets punished for proximity alone. That dispersion is the only tradeable thing here, and identifying it requires reading architecture diagrams instead of headlines. Liquidity is the only truth in a fragmented chain, and the liquidity in this correction sits in the mispriced cohort, not in the headline.
The second contrarian point: scope-limiting language has a cost. If a downstream team reads chain unaffected and stops its internal review, and that team happens to run the off-chain-verify-then-submit pattern, the risk was transferred rather than removed. A well-run advisory says what is unaffected and also says who should still check. This one did the first and skipped the second.
The third: the genuinely important questions are all unanswered. Internal discovery or external report? Time from discovery to fix? Was a postmortem published? Was the fix backported to the 6.0.x through 6.7.x branches? Were downstream fork maintainers notified? Is there a CVE? Six questions, zero answers in the material available. Bugs are inevitable in cryptographic infrastructure. The response curve is the capability signal, and a response curve with no coordinates tells me nothing about governance maturity relative to the ecosystem weight SP1 now carries as a dependency rather than a product.
What I am doing with this, concretely. Any system in my mandate that verifies proofs in Rust before an on-chain action gets flagged for architecture review this week, not next quarter. Any dependency pinned between 6.0.0 and 6.8.0 gets an explicit version check rather than a we are on latest assertion from a team lead who has never opened the lockfile. Any negative test asserting that an invalid witness is rejected gets re-run against a patched verifier, because a green test produced inside the vulnerable window is not evidence of anything except that the test suite was also compromised.
When I built the Python spread tracker in January 2024 to arbitrage the ETF premium against the Coinbase index, the rule that made it profitable was not clever modeling. It was that I would not act on a signal unless two independent feeds agreed within tolerance. Differential verification is not a luxury feature. It is the whole control. The zkVM space has spent years competing on proof generation speed while leaving implementation diversity as an afterthought, and this disclosure is what that afterthought costs.
And the forward question worth sitting with: when zkVM proofs eventually become the audit evidence for reserves, for solvency, for regulatory attestation, whose signature is on the attestation when the verifier says yes to a false statement? The algorithm executes, but the human decides. Right now the humans are deciding without a fix version number.