Skip to content

BQFC: reject non-reduced decompressed forms in strict mode#335

Merged
hoffmang9 merged 4 commits into
Chia-Network:mainfrom
richardkiss:fix-b0-canonical-check
May 13, 2026
Merged

BQFC: reject non-reduced decompressed forms in strict mode#335
hoffmang9 merged 4 commits into
Chia-Network:mainfrom
richardkiss:fix-b0-canonical-check

Conversation

@richardkiss
Copy link
Copy Markdown
Contributor

@richardkiss richardkiss commented Mar 12, 2026

Fixes #334.

Problem

bqfc_verify_canon performs a self-consistency check — encode(decode(X)) == X — rather than a uniqueness check — encode(reduce(decode(X))) == X.

For forms with g > 1, the b0 field can be inflated by any multiple of a' = a/g and still round-trip through bqfc_compr, because the xgcd_partial Euclidean path is unaffected and b0 = floor(|b|/a') picks up the inflated quotient.

Concretely (1024-bit discriminant, g=2, g_size=0): byte 99 (b0) accepts 64 distinct values — b0, b0+4, b0+8, … — instead of 1. DeserializeForm silently reduces the form via from_abd, so Wesolowski verification passes with the correct (reduced) y. The proof is malleable: the same mathematical VDF output has 64 valid serializations.

Fix

Add a strict flag to BQFC form deserialization.

When strict=true, after bqfc_decompr computes (out_a, out_b), assert |out_b| <= out_a. A reduced binary quadratic form requires |b| <= a; if this is violated the encoding cannot be canonical.

When strict=false, preserve the historical behavior and accept these forms for compatibility with existing consensus behavior.

The check is a single mpz_cmpabs call — no need to compute c or run the full Pulmark reduction.

if (strict && mpz_cmpabs(out_b, out_a) > 0) {
    ret = -1;
    goto out;
}

Why this is the right place

The root cause is that bqfc_decompr can produce a non-reduced (out_a, out_b), and bqfc_verify_canon compares against the encoding of that non-reduced form rather than the reduced representative. Adding the optional strict bounds check here:

  1. Costs one GMP comparison per strict deserialization.
  2. Lets callers explicitly choose strict canonical decoding versus historical lenient compatibility.
  3. Fixes the issue at the correct semantic layer when strict mode is selected: decompression guarantees a reduced form or fails.

This PR does not change default consensus behavior unless consensus callers opt into strict=true.

Regression test

Added strict/lenient regression coverage to proof_deserialization_regression_test.cpp using a real Chia mainnet vector (block 309155, CC infusion-point VDF). The canonical b0=0x01 is accepted; inflated b0=0x05 and b0=0x09 are rejected with strict=true and accepted with strict=false.

The Python module also exposes bqfc_deserialize(..., strict=True) for testing and differential fuzzing. Its default is strict, and strict=False preserves the historical behavior.

Testing

Discovered via differential fuzzing of chia-vdf-verify (pure-Rust reimplementation) against chiavdf, where Rust's stricter is_reduced() check exposed the discrepancy. See the characterization in issue #334.

Made with Cursor


Note

Medium Risk
Touches BQFC form decompression/deserialization used in proof verification; the new strict path can change acceptance of previously-valid encodings and introduces a new API parameter that callers must handle correctly.

Overview
Adds an optional strict mode to BQFC decompression/deserialization (bqfc_decompr/bqfc_deserialize) that rejects decoded forms where |b| > a, closing a b0-inflation malleability gap while preserving legacy behavior when strict=false.

Updates call sites to thread the new flag (e.g., hardware init uses lenient mode) and extends regression coverage to assert strict rejection vs lenient acceptance of inflated b0 encodings; also exposes a new Python binding chiavdf.bqfc_deserialize(..., strict=True) plus pytest coverage, and ignores src/build/ in .gitignore.

Reviewed by Cursor Bugbot for commit bd82b74. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI review requested due to automatic review settings March 12, 2026 23:20
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens compressed quadratic-form deserialization to eliminate a known malleability vector in the b0 field, and adds a regression test using a real mainnet-derived vector.

Changes:

  • Add an additional validity check during bqfc_decompr to reject certain non-canonical decodings.
  • Add a regression test that mutates b0 (inflation by +4/+8) and asserts deserialization rejection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/proof_deserialization_regression_test.cpp Adds a regression test covering b0 inflation malleability using a fixed mainnet vector.
src/bqfc.c Adds a post-decompression bound check intended to reject inflated b0 encodings that previously passed canonical verification.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/bqfc.c Outdated
Comment thread src/proof_deserialization_regression_test.cpp Outdated
Comment thread src/proof_deserialization_regression_test.cpp Outdated
Add a `bool strict` parameter to bqfc_decompr and bqfc_deserialize.

When strict=true, reject decompressed forms where |b| > a (the reduced
form invariant). This catches inflated b0 encodings that pass the
self-consistency round-trip check but violate uniqueness.

When strict=false (the default), preserve the historical behaviour for
consensus compatibility with existing chain data.

DeserializeForm() defaults to strict=false so all existing callers are
unaffected. The flag can be toggled per-call site when a hard fork
activates stricter validation.

Tests cover both modes using the mainnet block 309155 vector.

Made-with: Cursor
@richardkiss richardkiss force-pushed the fix-b0-canonical-check branch from 027fd06 to c3f6b05 Compare April 29, 2026 01:45
Allows differential testing of strict vs lenient BQFC deserialization
from Python.

Made-with: Cursor
Return signed big-endian bytes from the Python bqfc_deserialize helper to match chia-vdf-verify, and test strict-by-default rejection with lenient opt-out.

Made-with: Cursor
@richardkiss richardkiss changed the title bqfc_decompr: reject decompressed forms where |b| > a (fixes b0 malleability) BQFC: reject non-reduced decompressed forms in strict mode May 13, 2026
@hoffmang9
Copy link
Copy Markdown
Member

We're going to get around to a chiavdf release one of these days. Should this get in?

Happy to pick it up and finish it if so.

@richardkiss
Copy link
Copy Markdown
Contributor Author

We're going to get around to a chiavdf release one of these days. Should this get in?

Happy to pick it up and finish it if so.

This is entirely AI generated, with guidance from me. Seems like non-canonical proofs are accepted, as was discovered by differential fuzzing with the newly created rust vdf crate. This lets us tighten this gap when HF2 kicks in. A similar knob exists in the rust vdf api.

@hoffmang9
Copy link
Copy Markdown
Member

Close and re-open to reset baseline CI status

@hoffmang9 hoffmang9 closed this May 13, 2026
@hoffmang9 hoffmang9 reopened this May 13, 2026
Update the regression test call site for the strict deserializer argument and keep the Python test within lint limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hoffmang9 hoffmang9 enabled auto-merge (squash) May 13, 2026 02:05
Copy link
Copy Markdown
Member

@hoffmang9 hoffmang9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@hoffmang9 hoffmang9 merged commit 652319d into Chia-Network:main May 13, 2026
67 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bqfc_verify_canon allows non-canonical b0 encodings — class-equivalent forms accepted as valid proof elements

3 participants