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

90220 bc medium dilithium signature verification is undercharged by stale base extrinsic weight

Submitted on Aug 23rd 2026 at 19:48:30 UTC by @abarbatei for Audit Comp | Quantus

  • Report ID: #90220

  • Report Type: Blockchain/DLT

  • Report severity: Medium

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

  • Impacts:

    • Inaccuracies to the weight (similar to gas) accounting system that under or overcharge by more than 50% of the correct amount (and are not intentional as indicated by the program's terms)

Description

Brief/Intro

Quantus accepts signed transactions authenticated with ML-DSA Dilithium signatures, but the checked-in base extrinsic weight is still a generic Substrate no-op benchmark value of 108,157 ns. A valid ML-DSA-87 signed transaction performs account derivation and signature verification before dispatch while block-weight and fee accounting reserve only the stale base amount plus normal call and length components. The runnable proof measured the runtime-like ML-DSA-87 authentication cost at 264,126 ns, so the base component undercharges this mandatory work by 59.05% of the correct cost.

Vulnerability Details

The intended behavior is that each signed extrinsic reserves and pays for the compute needed to authenticate it. Otherwise a block can contain more verification work than the block weight model says it contains, and transaction fees no longer reflect the actual per-transaction cost.

Quantus changes the runtime signature type from the usual Substrate signature model to Dilithium. The runtime aliases at lib.rs#L111-L183 make DilithiumSignatureScheme the Signature, derive AccountId from that signature scheme, include CheckWeight in TxExtension, and use those types for UncheckedExtrinsic and SignedPayload. The accepted signature enum includes both ML-DSA-87 and ML-DSA-65 variants at types.rs#L51-L74.

Signature verification is not a cheap constant check. DilithiumSignatureScheme::verify derives the expected account from the public key carried in the signature and then calls the selected ML-DSA verifier.

DilithiumSignatureScheme::verify lines 160-183

The account derivation used by that check hashes the Dilithium public key at DilithiumSigner::into_account lines 194-199, so the authentication path includes both account derivation and the selected ML-DSA verification.

The same check is reached before dispatch during block import. Executive::do_apply_extrinsic decodes the unchecked extrinsic, verifies the signature through check, then obtains dispatch info and applies the already checked extrinsic.

Executive::do_apply_extrinsic lines 883-899

Transaction-pool validation also performs the signed check first at Executive::validate_transaction lines 983-1004. This makes the work attacker reachable through ordinary valid signed transactions.

The block-weight path does not add a Dilithium-specific verification charge. CheckWeight accounts for the dispatch info, the configured base_extrinsic, and encoded length as proof size.

calculate_consumed_weight lines 162-166

That configured base is the problem. The checked-in value was generated by a 2024 generic Substrate overhead benchmark and sets the no-op extrinsic overhead to 108,157 ns.

extrinsic_weights.rs#L18-L56

BlockWeights::builder installs constants::ExtrinsicBaseWeight::get() as each non-mandatory class's base_extrinsic at BlockWeights::builder lines 343-355, and the Quantus runtime uses BlockWeights::with_sensible_defaults at configs/mod.rs#L80-L95. Fees use the same base value through T::BlockWeights::get().get(class).base_extrinsic at Pallet::compute_fee_raw lines 668-680, with a 1:1 compute-weight fee mapping configured at configs/mod.rs#L477-L486.

A focused timing proof measured the runtime-like ML-DSA-87 authentication component at 264,126 ns. Compared with the configured 108,157 ns base, the uncharged portion is 155,969 ns. Using the correct component as the denominator, the undercharge is 155,969 / 264,126 = 59.05%. This already exceeds the program's 50% threshold before including any remaining non-signature extrinsic overhead.

There is useful counterevidence: the current benchmark builder creates Dilithium87 signed system.remark extrinsics at RemarkBuilder::build lines 42-53 and create_benchmark_extrinsic lines 99-161. Regenerating and checking in the overhead artifact may therefore be enough to fix this. The audited commit still ships the stale 108,157 ns artifact, so the live configured accounting is under the measured ML-DSA-87 authentication cost.

Impact Details

An untrusted user can create valid ML-DSA-87 signed low-dispatch-cost extrinsics and submit many of them. Each included extrinsic forces importing nodes to perform Dilithium public-key account derivation and ML-DSA-87 signature verification, but the chain only accounts for the stale generic base, normal dispatch weight, and length component. This causes block-weight and transaction-fee under-accounting for each such transaction.

The impact is medium: "Inaccuracies to the weight (similar to gas) accounting system that under or overcharge by more than 50% of the correct amount (and are not intentional as indicated by the program's terms)." The measured ML-DSA-87 runtime-like authentication component is undercharged by 59.05% of its correct cost.

This should not be overstated as proven network shutdown. Block length is capped and the available evidence proves the weight and fee inaccuracy, not a separate total liveness failure.

Mitigation

Regenerate ExtrinsicBaseWeight for the Quantus runtime and checked-in node at the audited configuration, using the Dilithium87 signed no-op extrinsic path. Check in the regenerated frame/support/src/weights/extrinsic_weights.rs value and make the overhead benchmark part of release CI whenever the runtime signature type, transaction extension set, or signature verification implementation changes.

If the base extrinsic weight is not the desired accounting location, add an explicit signed-extrinsic authentication weight that covers the worst accepted signature variant, including public-key account derivation and ML-DSA verification, and make sure both block-weight admission and transaction-payment fees use it.

References

Proof of Concept

Setup Assumptions

  • The audited chain repository is checked out at commit 3b243b870a5442a6e5f443056f67bbba87d472f5.

  • The proof files are installed into runtime/tests/poc_dilithium_base_weight_undercharge/ for the run, with offline-vendor/ adjacent to Cargo.toml.

  • The proof uses local path dependencies for the in-scope audit-comp-quantus-qp-poseidon and audit-comp-quantus-qp-rusty-crystals repositories. The offline run below uses the vendored registry patches in offline-vendor/ so Cargo does not need network access.

Execution Command

Proof Manifest

Proof Source

Observed Proof Output

The final assertion first confirms that the checked-in base-extrinsic artifact is the stale 108_157 ns value, then asserts that the valid ML-DSA-87 authentication path is undercharged by more than 50% of the measured correct cost. In the recorded run, the measured authentication cost was 264_126 ns; the uncharged portion was 155_969 ns, which is 59.05% of the measured cost.

Was this helpful?