For the complete documentation index, see llms.txt. This page is also available as Markdown.

88485 bc high unverified nullifier public inputs bypass wormhole semantic txpool deduplication

Submitted on Aug 13th 2026 at 12:35:10 UTC by @CarlosMB for Audit Comp | Quantus

  • Report ID: #88485

  • Report Type: Blockchain/DLT

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-quantus-chain

  • Impacts:

    • Causing network processing nodes to process transactions from the mempool beyond set parameters

Description

Summary

Wormhole exit transactions are unsigned, fee-free, and propagated through the transaction pool.

Quantus deliberately moved expensive Plonky2 verification out of validate_unsigned() and into pre_dispatch() to prevent every gossiped junk proof from immediately forcing expensive ZK verification.

Because proofs are therefore not cryptographically verified at pool-admission time, Quantus introduced a second mitigation: different representations of the same logical exit are deduplicated using a provides tag derived from the proof's nullifier set.

The vulnerability is that these nullifiers are themselves unverified public inputs at the moment the deduplication key is generated.

An attacker can therefore take one existing serialized Wormhole proof, modify only one nullifier public-input field, and re-serialize it.

Each mutation:

  1. remains canonically serialized;

  2. passes the cheap Wormhole pool checks;

  3. receives a different provides tag;

  4. is accepted as an independent transaction-pool entry;

  5. is propagatable;

  6. requires no account, signature, balance, or transaction fee;

  7. and is rejected only later in pre_dispatch(), after full Plonky2 verification has executed.

A runnable regression PoC confirms that one unchanged cryptographic proof can generate 32 independently admissible pool identities, while every variant subsequently fails full ZK verification.

This directly bypasses the nullifier-based semantic deduplication introduced to prevent fee-free junk-proof amplification.


Root Cause

The transaction pool uses attacker-controlled, cryptographically unauthenticated nullifier public inputs as the semantic identity of unsigned Wormhole exit transactions.

The nullifier is trusted for deduplication before the ZK verifier establishes that the nullifier actually belongs to the submitted proof.

As a result, the attacker controls the field that determines whether two fee-free proof submissions are treated as duplicates or as independent transactions.


Vulnerability Details

1. Full ZK verification is intentionally deferred

validate_unsigned() performs only cheap pre-validation:

This is intentional.

Quantus previously performed full Plonky2 verification during unsigned transaction-pool validation. The security hardening commit that changed this behavior explains that unsigned, fee-free gossip could otherwise force approximately 17–25 ms of verification CPU for each junk proof variant.

The new design therefore:

  • performs cheap pre-validation in validate_unsigned();

  • performs full verification in pre_dispatch();

  • and relies on nullifier-based semantic deduplication to prevent multiple representations of one logical proof from creating independent pool work.

Full cryptographic verification occurs here:

and eventually:

Therefore, any values extracted from proof.public_inputs during validate_unsigned() have not yet been cryptographically authenticated.

2. The semantic deduplication key is entirely derived from unverified nullifiers

Wormhole generates the txpool provides tag using:

The code describes this as semantic transaction-pool deduplication whose objective is effectively:

rather than:

However, the nullifiers used in this hash come directly from the still-unverified proof public inputs.

3. Cheap validation only checks whether the forged nullifier looks usable

Before ZK verification, the nullifier is checked for properties such as:

  • correct layout;

  • non-zero value;

  • no duplicate inside the bundle;

  • not already present in UsedNullifiers;

  • not already claimed elsewhere in the bundle.

These conditions only establish that the attacker-declared value is fresh.

They do not establish:

Only Plonky2 verification establishes that property, and that happens later.

Therefore the attacker can select another fresh canonical nullifier value and pass all cheap checks.

4. Existing Quantus tests already confirm public inputs can be modified before verification

The existing test:

mutates a proof public input, re-serializes the proof and confirms that the resulting cryptographically invalid proof still passes cheap pool validation.

The test explicitly describes this as:

This confirms that changing a public input does not inherently break:

  • proof deserialization;

  • canonical re-serialization;

  • or validate_unsigned().

That existing mitigation is safe because changing an amount does not change the nullifier-based provides tag.

The vulnerability appears when the attacker modifies the nullifier public input itself, because this is simultaneously:

  1. still unauthenticated; and

  2. the input controlling semantic deduplication.


Deterministic Attack Path

The attacker then changes the nullifier again:

No new ZK proof needs to be generated.

Proof of Concept

The following regression test was added to:

Run:

Result:

What the PoC proves

The PoC deterministically proves all vulnerability-specific properties:

Specifically, it proves that:

  1. modifying a nullifier PI preserves cheap pool validity;

  2. each modification bypasses semantic provides deduplication;

  3. each variant reaches the full-verification gate;

  4. every variant fails only when the expensive cryptographic verification is executed.

What the PoC does not claim

This regression test is not a full-node networking benchmark.

It does not claim to measure a specific block-delay percentage or to prove complete network halt.

Those properties are not required for the reported BC-H03 impact.

The reported issue is that the anti-DoS processing bound can be bypassed, allowing one proof to create arbitrarily many independently processable fee-free mempool transactions.

The processing behavior after pool admission follows deterministically from the production proposer implementation described below.


Why Each Variant Causes Independent Proposer Processing

Quantus uses sc-basic-authorship for proposal construction.

For each ready transaction, the proposer calls:

A forged Wormhole transaction therefore reaches pre_dispatch(), where the Plonky2 verification executes.

When verification fails, the proposer treats the transaction as invalid and removes it.

Importantly, this failure path does not trigger:

That counter applies to resource-exhaustion/block-capacity cases.

For ordinary validity failures, the proposer performs the equivalent of:

Therefore a ready pool containing attacker-generated variants can result in:

until the iterator is exhausted or the proposal deadline is reached.


Why Invalid-Transaction Banning Does Not Stop the Attack

After one variant fails, its exact transaction hash can be marked invalid/banned.

This does not prevent further variants because changing the nullifier changes:

  1. the serialized proof bytes;

  2. the extrinsic hash;

  3. the semantic provides tag.

Therefore:

The attacker does not need to replay an already-banned transaction.


Why QPoW's Rebuild Rate Limit Does Not Stop the Attack

QPoW limits transaction-triggered candidate rebuilds to approximately one every two seconds.

This mitigation remains effective and is not being bypassed.

The attack does not require:

A single proposal build iterates over multiple ready transactions.

Therefore one rebuild can itself process many forged Wormhole variants sequentially.


Impact

This vulnerability bypasses the security mechanism specifically added to prevent fee-free invalid Wormhole proofs from creating independent transaction-processing work.

The relevant asymmetry is:

The production benchmark constants estimate private-batch ZK verification at approximately:

per proof.

The private-batch fixture is approximately:

and the default transaction-pool capacity is approximately:

Quantus provides approximately a 10-second proposal duration, with basic-authorship reserving roughly 10% for finalization.

As a capacity estimate only, approximately 560 private-proof variants correspond to:

and approximately:

of proof data.

This is below the default pool byte limit.

These estimates are not necessary to establish the vulnerability and are not presented as an E2E timing measurement. They demonstrate that the amount of independently processable junk permitted by the bypass is meaningful relative to the configured processing window.

The attack is additionally:

  • unauthenticated;

  • unsigned;

  • fee-free;

  • propagatable;

  • repeatable from one proof;

  • and does not require generating new valid ZK proofs.

This maps directly to:

BC-H03 — Causing processing nodes to process mempool transactions beyond set parameters


Why This Is Not Intended Behavior

Deferring full verification to pre_dispatch() is intentional.

Allowing arbitrary independent identities derived from one proof is not.

The security change that introduced the current architecture explicitly combined:

because either component alone does not protect against fee-free invalid-proof amplification.

The bypass breaks the second part of that security boundary.

The issue therefore is not:

"invalid proofs can reach pre_dispatch"

which is expected.

The issue is:

"one underlying invalid proof can be transformed into arbitrarily many independently admissible transactions because the deduplication identity is itself attacker-controlled before verification."

That defeats the intended semantic deduplication property.


Do not use cryptographically unauthenticated public inputs as the sole anti-DoS identity for fee-free Wormhole proof submissions.

Possible mitigations include:

  • introducing a separately bounded queue for unverified Wormhole proofs;

  • applying global/per-peer limits to unverified Wormhole transactions;

  • requiring authentication, fees, or a refundable bond for proof submission;

  • moving proof verification into a bounded admission service with verification-result caching;

  • preventing unrestricted propagation of proofs that have not yet been cryptographically authenticated.

Regression tests should additionally verify that arbitrary mutations of public inputs cannot transform one underlying proof into unbounded independent transaction-pool identities.

Was this helpful?