Skip to content

[PQ-Accounts] SHAKE hint and direct variants - #1730

Open
ericnordelo wants to merge 6 commits into
mainfrom
feat/add-post-quantum-accounts
Open

[PQ-Accounts] SHAKE hint and direct variants#1730
ericnordelo wants to merge 6 commits into
mainfrom
feat/add-post-quantum-accounts

Conversation

@ericnordelo

@ericnordelo ericnordelo commented Aug 7, 2026

Copy link
Copy Markdown
Member

Fixes #1725

Summary

  • Adds Falcon512AccountComponent with SHAKE-256 hint and direct verification strategies for canonical 29-felt Falcon-512 public keys.
  • Adds the upgradeable Falcon512ShakeAccountUpgradeable and Falcon512ShakeDirectAccountUpgradeable presets with SRC9 outside execution and self-authorized class upgrades.
  • Adds felt-array deployment and public-key interfaces, key rotation, package documentation, and focused cryptographic and account tests.

The verifiers implement the SHAKE-256 verification relation from the FALCON submission selected by NIST using contract-specific felt encodings. They are not FN-DSA (FIPS 206) implementations.

PR Checklist

  • Tests
  • Documentation
  • Added entry to CHANGELOG.md
  • Tried the feature on a public network

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5edd628c-6768-4344-8599-2e91c600d050

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The change adds Falcon-512 SHAKE account variants, pure Cairo hashing, packing, modular arithmetic, generic and optimized NTT implementations, account interfaces, validation logic, comprehensive tests, and release-mode CI coverage.

Changes

Falcon-512 account support

Layer / File(s) Summary
Hashing and Falcon data primitives
packages/account/src/falcon_512/hashing/*, packages/account/src/falcon_512/packing.cairo, packages/account/src/falcon_512/zq.cairo
Adds SHAKE-256, Falcon hash-to-point, canonical key and signature packing, and modular arithmetic helpers with tests.
Generic and optimized NTT processing
packages/account/src/falcon_512/ntt/*
Adds configurable generic NTT transforms, Falcon-512 roots and permutations, inverse transforms, and generated fast forward transforms.
Verification and account contracts
packages/account/src/falcon_512.cairo, packages/account/src/falcon_512/falcon.cairo, packages/account/src/falcon_512/account.cairo, packages/account/src/falcon_512/falcon_512_shake*.cairo, packages/interfaces/src/account/accounts.cairo
Adds hint-based and direct verification, shared account validation and execution behavior, two Falcon account contracts, and felt-array deployment and public-key interfaces.
Tests and project integration
.github/workflows/test.yml, Scarb.toml, packages/account/Scarb.toml, packages/account/src/tests/*, packages/interfaces/README.md, packages/account/README.md, LICENSE
Adds release-mode production tests, Falcon fixtures and account/NTT test suites, dependency and feature configuration, account documentation, interface listings, and third-party attribution.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Account
  participant Verifier
  participant SHAKE256
  participant NTT
  Account->>Verifier: submit transaction signature
  Verifier->>SHAKE256: derive message point from message and salt
  Verifier->>NTT: transform and verify Falcon polynomial values
  NTT-->>Verifier: return norm and product checks
  Verifier-->>Account: return validation status
Loading

Poem

A rabbit hops through SHAKE’s bright stream,
Packs keys neatly into a felt-bound dream.
NTT roots whirl, signatures align,
Two Falcon accounts now securely shine.
Tests keep watch through each release night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding SHAKE hint and direct Falcon account variants.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-post-quantum-accounts

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🧪 Cairo Contract Size Benchmark Diff

BYTECODE SIZE (felts) (limit: 81,920 felts)

No changes in felts.

SIERRA CONTRACT CLASS SIZE (bytes) (limit: 4,089,446 bytes)

No changes in bytes.

This comment was generated automatically from benchmark diffs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (7)
packages/account/README.md (1)

13-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider listing the new Falcon interfaces in the sections below.

The new bullets describe the two Falcon accounts. The Interfaces section still lists only ISRC6 and ISRC9_V2. The tests use IFeltArrayPublicKey and IFeltArrayDeployable, which are new public interfaces. Add them so readers can find the felt-array key and deploy entrypoints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/account/README.md` around lines 13 - 22, Update the README’s
Interfaces section to list the new public interfaces IFeltArrayPublicKey and
IFeltArrayDeployable alongside ISRC6 and ISRC9_V2, reflecting the felt-array key
and deploy entrypoints used by the Falcon accounts.
packages/account/src/tests/test_falcon_512.cairo (1)

375-391: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bare #[should_panic] in four negative tests. These tests pass on any panic, including a panic from an unrelated cause such as calldata deserialization or a future assertion added earlier in the call path. The rest of both files already pins the panic reason with #[should_panic(expected: ...)], so these four sites are the inconsistent ones.

  • packages/account/src/tests/test_falcon_512.cairo#L375-L391: add the expected panic message to test_constructor_rejects_wrong_public_key_length and test_constructor_rejects_noncanonical_public_key, so each test proves the length check and the canonicality check specifically.
  • packages/account/src/tests/test_falcon_512_ntt.cairo#L143-L153: add the expected panic message to test_forward_roots_reject_unsupported_degree and test_inverse_roots_reject_unsupported_degree, using the real message emitted by get_even_roots_felt and get_scaled_inv_roots.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/account/src/tests/test_falcon_512.cairo` around lines 375 - 391, The
four negative tests currently accept any panic; pin each to the specific
expected panic message. In
packages/account/src/tests/test_falcon_512.cairo:375-391, update
test_constructor_rejects_wrong_public_key_length and
test_constructor_rejects_noncanonical_public_key with the messages emitted by
the public-key length and canonicality checks. In
packages/account/src/tests/test_falcon_512_ntt.cairo:143-153, update
test_forward_roots_reject_unsupported_degree and
test_inverse_roots_reject_unsupported_degree with the actual messages from
get_even_roots_felt and get_scaled_inv_roots.
packages/account/src/falcon_512.cairo (1)

24-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the slice offsets from named constants and share the decoding.

The literals 29 and 31 in the slices repeat the layout that PUBLIC_KEY_FELTS, SIGNATURE_FELTS and DIRECT_SIGNATURE_FELTS already encode. The two verify implementations also duplicate the public key, s1 and salt decoding. If the polynomial slot count changes, the length gate and the slices can desynchronize.

♻️ Proposed refactor
+/// Felts per packed Z_q polynomial (28 full slots + 1 partial slot).
+pub(crate) const POLY_FELTS: u32 = packing::PACKED_SLOTS;
+/// Felts holding the 40-byte salt.
+const SALT_FELTS: u32 = 2;
+
 /// Number of felts in a packed Falcon-512 public key.
-pub(crate) const PUBLIC_KEY_FELTS: u32 = 29;
+pub(crate) const PUBLIC_KEY_FELTS: u32 = POLY_FELTS;
 
 /// Number of felts in a Falcon-512 signature carrying a product hint.
-pub(crate) const SIGNATURE_FELTS: u32 = 60;
+pub(crate) const SIGNATURE_FELTS: u32 = DIRECT_SIGNATURE_FELTS + POLY_FELTS;
 
 /// Number of felts in a hint-free Falcon-512 signature.
-pub(crate) const DIRECT_SIGNATURE_FELTS: u32 = 31;
+pub(crate) const DIRECT_SIGNATURE_FELTS: u32 = POLY_FELTS + SALT_FELTS;

Then reuse one helper for the shared prefix:

/// Decodes `h_ntt`, `s1` and the message point shared by both signature layouts.
fn decode_common(
    message_hash: felt252, public_key: Span<felt252>, signature: Span<felt252>,
) -> Option<(Array<u16>, Array<u16>, Array<u16>)> {
    let h_ntt = packing::unpack_512_u16(public_key)?;
    let s1 = packing::unpack_512_u16(signature.slice(0, POLY_FELTS))?;
    let message_point = hash_to_point_shake_512(
        message_hash, *signature.at(POLY_FELTS), *signature.at(POLY_FELTS + 1),
    )?;
    Some((h_ntt, s1, message_point))
}

and slice the hint with signature.slice(DIRECT_SIGNATURE_FELTS, POLY_FELTS).

Also applies to: 36-52, 76-88

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/account/src/falcon_512.cairo` around lines 24 - 31, Derive all
signature and public-key slice offsets from the existing layout constants,
including the hint slice, instead of hardcoded 29 and 31 values. Add a shared
decode_common helper for public-key, s1, and message-point decoding, then update
both verify implementations to reuse it while preserving their distinct
signature-layout handling.
packages/account/src/falcon_512/hashing/shake256.cairo (1)

156-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Gate shake256 behind #[cfg(test)] or add a non-test caller.

shake256 has no non-test caller. Production code imports only keccak_f1600, and all calls are in the test module.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/account/src/falcon_512/hashing/shake256.cairo` around lines 156 -
157, Update the `shake256` function visibility so it is compiled only for tests
using the project’s test configuration attribute, unless a real production
caller is added. Keep `keccak_f1600` and the existing test calls unchanged.
packages/account/src/falcon_512/ntt/engine.cairo (1)

357-360: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reset bits with bound in the last-level reduction.

The branch resets bound but leaves bits stale. bits is not read after this point, so behavior is correct today. A future change that reads bits after the last level would use a wrong value. Keep the two tracked variables consistent.

♻️ Proposed change
     if bits + growth_bits > SAFE_BITS {
         cur = reduce_pass(cur.span(), q_nz);
+        bits = qbits;
         bound = q_felt;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/account/src/falcon_512/ntt/engine.cairo` around lines 357 - 360,
Update the last-level reduction branch in the NTT engine to reset bits
consistently when bound is reset after reduce_pass. Preserve the existing
reduction flow and ensure both tracked values reflect the new bound for any
subsequent use.
packages/account/src/falcon_512/ntt/falcon512.cairo (1)

34-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document or guard the degree precondition.

config_for_degree accepts any n. If n is not a power of two in [4, 512], config_with_perm calls get_even_roots_felt with an unsupported size and panics with "no root table for degree". The failure message does not name the caller argument. Add an explicit assertion so misuse fails with a clear reason.

♻️ Proposed guard
 pub fn config_for_degree(n: u32, levels: u32) -> NttConfig {
+    assert!(n >= 4 && n <= 512, "config_for_degree: unsupported degree");
     let mut perm: Array<u16> = array![];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/account/src/falcon_512/ntt/falcon512.cairo` around lines 34 - 50,
Add an explicit precondition at the start of config_for_degree validating that n
is a power of two within the supported range [4, 512], and assert with a message
that includes the supplied n when invalid. Keep the existing permutation
construction and config_with_perm flow unchanged for valid degrees.
packages/account/src/falcon_512/account.cairo (1)

155-178: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Read the public key once per validation call.

validate_transaction and is_valid_signature both call read_public_key, which performs one storage read per stored felt. The Falcon-512 key is 29 felts, so each validation costs 29 storage reads plus the vector length read.

The key is immutable after the constructor. Consider caching it in a single packed storage layout, or document the cost so integrators can budget for it. This is an observation about gas cost, not a correctness defect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/account/src/falcon_512/account.cairo` around lines 155 - 178, Reduce
repeated public-key storage reads across validate_transaction and
is_valid_signature by caching the immutable Falcon-512 key in a single packed
storage representation initialized by the constructor, then have read_public_key
reuse that cached value. If the existing storage model prevents caching,
document the 29-felt plus length-read cost for integrators instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/account/src/falcon_512/account.cairo`:
- Around line 141-152: Guard the public initializer method against repeated
calls by checking that self.public_key is empty before validating or appending
the key. Add and use a dedicated initialization error constant alongside the
existing Errors constants, ensuring a second call fails without mutating the
stored key.

In `@packages/account/src/tests/falcon_512_fixture.cairo`:
- Around line 8-11: Correct the documentation comment above msg() to describe
1222479111879746474823 as the encoded benchmark message “BENCH_MSG,” not its
hash; leave the function and returned value unchanged.

In `@packages/account/src/tests/test_falcon_512_ntt.cairo`:
- Around line 136-141: Update test_intt_reduces_before_last_level to pass the
nonzero input ntt([1, 2, 3, 4]) to intt, using input_bits = 111 and input_bound
= 12289 with the existing degree-4 configuration. Preserve the assertion that
the recovered values equal the original [1, 2, 3, 4] values.

---

Nitpick comments:
In `@packages/account/README.md`:
- Around line 13-22: Update the README’s Interfaces section to list the new
public interfaces IFeltArrayPublicKey and IFeltArrayDeployable alongside ISRC6
and ISRC9_V2, reflecting the felt-array key and deploy entrypoints used by the
Falcon accounts.

In `@packages/account/src/falcon_512.cairo`:
- Around line 24-31: Derive all signature and public-key slice offsets from the
existing layout constants, including the hint slice, instead of hardcoded 29 and
31 values. Add a shared decode_common helper for public-key, s1, and
message-point decoding, then update both verify implementations to reuse it
while preserving their distinct signature-layout handling.

In `@packages/account/src/falcon_512/account.cairo`:
- Around line 155-178: Reduce repeated public-key storage reads across
validate_transaction and is_valid_signature by caching the immutable Falcon-512
key in a single packed storage representation initialized by the constructor,
then have read_public_key reuse that cached value. If the existing storage model
prevents caching, document the 29-felt plus length-read cost for integrators
instead.

In `@packages/account/src/falcon_512/hashing/shake256.cairo`:
- Around line 156-157: Update the `shake256` function visibility so it is
compiled only for tests using the project’s test configuration attribute, unless
a real production caller is added. Keep `keccak_f1600` and the existing test
calls unchanged.

In `@packages/account/src/falcon_512/ntt/engine.cairo`:
- Around line 357-360: Update the last-level reduction branch in the NTT engine
to reset bits consistently when bound is reset after reduce_pass. Preserve the
existing reduction flow and ensure both tracked values reflect the new bound for
any subsequent use.

In `@packages/account/src/falcon_512/ntt/falcon512.cairo`:
- Around line 34-50: Add an explicit precondition at the start of
config_for_degree validating that n is a power of two within the supported range
[4, 512], and assert with a message that includes the supplied n when invalid.
Keep the existing permutation construction and config_with_perm flow unchanged
for valid degrees.

In `@packages/account/src/tests/test_falcon_512.cairo`:
- Around line 375-391: The four negative tests currently accept any panic; pin
each to the specific expected panic message. In
packages/account/src/tests/test_falcon_512.cairo:375-391, update
test_constructor_rejects_wrong_public_key_length and
test_constructor_rejects_noncanonical_public_key with the messages emitted by
the public-key length and canonicality checks. In
packages/account/src/tests/test_falcon_512_ntt.cairo:143-153, update
test_forward_roots_reject_unsupported_degree and
test_inverse_roots_reject_unsupported_degree with the actual messages from
get_even_roots_felt and get_scaled_inv_roots.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2bca1632-f2eb-4d2d-9c6f-db954491b7bc

📥 Commits

Reviewing files that changed from the base of the PR and between 619acaa and 3b9ee6a.

⛔ Files ignored due to path filters (1)
  • Scarb.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .github/workflows/test.yml
  • LICENSE
  • Scarb.toml
  • packages/account/README.md
  • packages/account/Scarb.toml
  • packages/account/src/falcon_512.cairo
  • packages/account/src/falcon_512/account.cairo
  • packages/account/src/falcon_512/falcon.cairo
  • packages/account/src/falcon_512/falcon_512_shake.cairo
  • packages/account/src/falcon_512/falcon_512_shake_direct.cairo
  • packages/account/src/falcon_512/hashing.cairo
  • packages/account/src/falcon_512/hashing/hash_to_point.cairo
  • packages/account/src/falcon_512/hashing/shake256.cairo
  • packages/account/src/falcon_512/ntt.cairo
  • packages/account/src/falcon_512/ntt/bitrev.cairo
  • packages/account/src/falcon_512/ntt/engine.cairo
  • packages/account/src/falcon_512/ntt/falcon512.cairo
  • packages/account/src/falcon_512/ntt/falcon512_fast.cairo
  • packages/account/src/falcon_512/ntt/roots_felt.cairo
  • packages/account/src/falcon_512/ntt/roots_scaled.cairo
  • packages/account/src/falcon_512/packing.cairo
  • packages/account/src/falcon_512/zq.cairo
  • packages/account/src/lib.cairo
  • packages/account/src/tests.cairo
  • packages/account/src/tests/falcon_512_fixture.cairo
  • packages/account/src/tests/test_falcon_512.cairo
  • packages/account/src/tests/test_falcon_512_ntt.cairo
  • packages/interfaces/README.md
  • packages/interfaces/src/account/accounts.cairo

Comment thread packages/account/src/falcon_512/account.cairo
Comment thread packages/account/src/tests/falcon_512_fixture.cairo Outdated
Comment thread packages/account/src/tests/test_falcon_512_ntt.cairo
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.88999% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...account/src/falcon_512/hashing/hash_to_point.cairo 98.50% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@bidzyyys bidzyyys linked an issue Aug 7, 2026 that may be closed by this pull request
@ericnordelo
ericnordelo requested review from bidzyyys and immrsd August 10, 2026 14:41
@bidzyyys

bidzyyys commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Review — falcon_512

  • packages/account/src/tests/test_falcon_512.cairo:578,587 — the two constructor rejection tests use a bare #[should_panic], so they also pass if the deploy fails for a reason unrelated to key validation; every other panic test in this file pins the reason → add expected: 'Falcon512: invalid public key' to both.
  • packages/account/src/falcon_512/account.cairo:81-82assert(sender.is_zero(), Errors::INVALID_CALLER) was carried over from account.cairo without the rationale comment it has there (// Avoid calls from other contracts + the issue link), and the assertion reads inverted without it → restore the two comment lines.
  • packages/account/src/falcon_512/account.cairo:382-399_set_public_key can only overwrite a key of the same length (the Vec never shrinks), so an instance upgraded to a future class with a different PUBLIC_KEY_FELTS could never rotate its key again: set_public_key would abort on INVALID_PUBLIC_KEY forever. Both shipped variants are 29 felts, but the presets expose upgrade → document the constraint on _set_public_key (or handle a length change explicitly).
  • packages/account/src/falcon_512/ntt/falcon512.cairo:14,71pub const Q: u16 = 12289 is never referenced (zq::Q is the one in use) and duplicates it, while config_with_perm writes the modulus as a bare literal in q_nz: 12289 → drop the duplicate const and use zq::Q/zq::Q32 for the literal.
  • packages/account/src/falcon_512/packing.cairo:21,213pub const VALS_PER_FELT is only used by pack_512, which is #[cfg(test)], while the sibling LAST_SLOT_VALS right below it is gated, so a test-only constant is exported from the crate; and pack_half multiplies by the literal 12289 one line after asserting against the imported Q → gate VALS_PER_FELT the same way and use Q.into() in the Horner step.
  • packages/account/src/falcon_512/ntt/engine.cairo:332-335 — the reduction guard before the last inverse level resets bound but not bits, unlike the identical guard in the loop above, which resets both. It is harmless today only because bits is dead from there on → reset bits = qbits too, or note why it is not needed.
  • docs/modules/ROOT/pages/utils/_class_hashes.adoc — every preset class hash changes here along with the documented compiler version (2.17.0 → 2.18.0), which is unrelated to Falcon and makes the table hard to verify against this diff → mention in the PR description that the non-Falcon hashes come from the compiler bump.
  • PR description — the template's checklist and the Fixes # link are missing; the body is only the generated summary → fill in the template so the tests/docs/CHANGELOG boxes are on record.

@immrsd immrsd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Left minor comments, apart from that LGTM

while let Some(lane) = lanes.pop_front() {
push_lane_words(*lane, two16, b256, q32, ref coeffs);
}
if coeffs.len() == 512 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

512 is used many times throughout the file, might be a good idea to extract it to a constant (COEFFS_TARGET_LEN or maybe something like that)

Comment on lines +81 to +88
let (_, r0) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v0).unwrap(), q_nz);
let (_, r1) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v1).unwrap(), q_nz);
let (_, r2) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v2).unwrap(), q_nz);
let (_, r3) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v3).unwrap(), q_nz);
let (_, r4) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v4).unwrap(), q_nz);
let (_, r5) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v5).unwrap(), q_nz);
let (_, r6) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v6).unwrap(), q_nz);
let (_, r7) = DivRem::div_rem(TryInto::<felt252, u128>::try_into(v7).unwrap(), q_nz);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does the compiler fail to derive types for TryInto here?

@@ -0,0 +1,45 @@
use openzeppelin_interfaces::src9::OutsideExecution;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

missing license

/// Its public-key and signature formats are contract-specific encodings for the FALCON submission
/// verification relation, rather than FN-DSA (FIPS 206) encodings.
#[starknet::contract(account)]
pub mod Falcon512ShakeAccountUpgradeable {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does the contract deliberately not use the with_component macros for OZ components? Do you think it might be helpful to reduce boilerplate code here?

newPublicKey: Array<felt252>,
signature: Span<felt252>,
) {
PublicKey::set_public_key(ref self, newPublicKey, signature);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seem like it should be possible to use receiver syntax here

Suggested change
PublicKey::set_public_key(ref self, newPublicKey, signature);
self.set_public_key(newPublicKey, signature);

Comment on lines +412 to +416
let mut index = 0;
while index != len {
public_key.append(self.Falcon512Account_public_key.at(index).read());
index += 1;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for i in 0..len { ... }

Comment on lines +162 to +165
while z != pad_len {
padded.append(0);
z += 1;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for z in 1..pad_len can be used here

let mut state = [0; 25];
let n_blocks = (l + pad_len) / RATE_BYTES;
let mut blk = 0;
while blk != n_blocks {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for in cycle

let mut out: Array<u8> = array![];
let span = bytes.span();
let mut i = 0;
while i != span.len() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for in cycle

fn test_shake256_multiblock() {
let mut input: Array<u8> = array![];
let mut i = 0;
while i != 200 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for in cycle

fn test_unpack_rejects_noncanonical_slot_in_each_unrolled_position() {
let packed = pack_512(pseudorandom_coeffs().span());
let mut bad_index = 0;
while bad_index != 7 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for in cycle


fn max_values() -> Array<u16> {
let mut values = array![];
while values.len() != 512 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

512 is used here as a magic value, consider extracting it to a constant

@immrsd immrsd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Full review of the Falcon-512 PQ account addition — findings verified against the actual sources, with the crypto core independently re-derived and the gas/size numbers measured on this branch (scarb/cairo 2.18.0).

Measured baseline (release profile):

CASM felts class bytes sierra gas / verify
Falcon512ShakeAccountUpgradeable 57,120 (69.7% of 81,920 cap) 2,159,881 ~52.5M
Falcon512ShakeDirectAccountUpgradeable 57,048 2,039,421 ~62.7M
AccountUpgradeable (baseline) 6,244 139,195

Cost split of one verification: SHAKE-256/hash_to_point ~36M (68%), generic intt ~15.5M (direct only), generated forward NTT ~1.6M, unpacking ~1.5M each.

Inline comments carry the individual findings (3 High, 8 Medium, 8 Low/Info). Two things that don't anchor to changed lines:

  • SRC9 self-call cost (Low, pre-existing design): SRC9Component::assert_valid_signature dispatches is_valid_signature through call_contract_syscall at the account's own address, re-entering the 57k-felt class and re-reading the whole 29-felt key (verified: CallContract: 2, StorageRead: 90 vs 1/30 on the direct path). Account-agnostic by design and not introduced here, but far more visible with this key size — the presets could call the component impl directly.
  • Minor: Errors::INVALID_PUBLIC_KEY reused for an unreachable stored-length invariant (account.cairo:397); SHIFT added per NTT output instead of per butterfly pair (−512 CASM felts available, two-line generator change); duplicated cfg-gated module/forward_ntt declarations in presets/src/lib.cairo and falcon.cairo; second_accept_ownership_hash() unused; DIRECT_SIGNATURE_FELTS hardcoded in preset tests because the constant is pub(crate); the intt(…, 111, 12289, …) NTT test passes deliberately inconsistent bits/bound args with no explanatory comment.

Verified sound (checked, not assumed): norm bound 34034726 = reference l2bound[9] with correct inclusive comparison; centering arithmetic; hint soundness (the generator proves the 512 evaluation points are distinct roots of x^512+1, so the NTT is a bijection and mul_hint is uniquely pinned to s1·h); strict canonical-encoding bijection with [0,q)^512 (all Acc*/DivT constants verified); SHAKE-256 round constants, ρ/π/θ/χ/ι and pad10*1 layout against FIPS 202; REJECT_BOUND = 5q; all overflow bounds (norm accumulator < 2^36, NTT lazy-reduction schedule peaks at 2^122.3 with 6 bits of headroom — do not reuse the generator for n=1024 without re-running the bound check). The checked-in NTT tables regenerate byte-for-byte from the script; the fixtures were decoded in Python and the Falcon relation re-verified externally. Replay protection, deploy front-running (key in constructor calldata → address derivation), access control, and graceful rejection of malformed input all check out.

Overall: the cryptographic core is correct and carefully built, but the upgrade-bricking hazard needs closing, the generated-code and SHAKE efficiency work plus a CASM/gas CI guard are needed before these presets are safe to evolve at 70% of the bytecode cap — and the hint-vs-direct question deserves an explicit decision before both variants become public API.

UpgradeableEvent: UpgradeableComponent::Event,
}

#[constructor]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 4 — High: upgrading into (or out of) this preset permanently bricks the account.

The component stores its key under Falcon512Account_public_key, distinct from Account_public_key / EthAccount_public_key, and initializer is constructor-only with no post-upgrade path. A user upgrading AccountUpgradeable → this preset (which the README's "can adopt verifier changes at the same account address" wording invites) lands with an empty key: every __validate__ and is_valid_signature fails, and set_public_key needs a self-call that itself needs a passing validation — funds are irrecoverably frozen. The reverse direction fails identically. Only the cross-variant Falcon↔Falcon upgrade is safe, and that's the only case the tests cover (test_falcon_512_accounts.cairo:196-208). Same applies to falcon_512_shake_direct_account.cairo.

Suggested fix: document loudly that these presets are upgrade-compatible only with each other, and/or assert a non-empty Falcon512Account_public_key inside upgrade().

type RemT = Falcon512Zq;
}

#[inline(always)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 4 — High: felt252_as_u128 silently truncates, and its Wide arm is ~20% dead bytecode.

The Wide((_, low)) => low arm discards the high 128 bits. It's provably unreachable for canonical inputs today (verified by re-running the generator's interval analysis), but a future caller passing a coefficient ≥ q would get a silently wrong (attacker-influenceable) NTT output instead of a panic — in the hint path that's the ingredient for an unsignalled forgery. Separately, the never-executed arm costs ~11,776 dead CASM felts across the 512 emission sites (~20.6% of the class), and the conversion overall is ~16,896 felts (~29.6%) plus ~3,070 steps per NTT call.

Suggested fix: have generate_ntt.py emit the op graph over BoundedInt with bounded_int_add/sub/mul (these lower to the same felt252_add/sub/mul CASM at zero extra cost) feeding straight into bounded_int_div_rem — removes ~16.9k CASM felts and ~3.1k steps per call while turning precondition violations into reverts. The helper traits would need re-exporting from openzeppelin_corelib_imports.

/// bits that wrap to the bottom (`hi`); they occupy disjoint positions, so `lo + hi`
/// is the rotation.
#[inline(always)]
fn rotl(x: u128, pow: felt252, two64: NonZero<u128>) -> u128 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 4 — High: SHAKE-256 is ~68% of every verification's cost and rotl is most of it.

Measured on this branch: one hash_to_point costs ~36M sierra gas — 68% (hint) / 57% (direct) of a full verification (~52.5M / ~62.7M). rotl runs ~5,900 times per verification (29× per round × 24 rounds × ~8.5 permutations), each doing a fallible felt252 → u128 try_into().unwrap(), a generic u128_safe_divmod, and a checked add (~3,000 gas apiece).

The PR already vendors the cheap tools (u128s_from_felt252 in falcon512_fast.cairo, bounded_int_div_rem in packing.cairo) but doesn't use them here.

Suggested fix: type the lane as BoundedInt<0, 0xffff_ffff_ffff_ffff>, split the shifted product with bounded_int_div_rem by UnitInt<2^64> and add the halves with a bounded add. Estimated saving: 10–15M gas per verification (~20–25% of total).

)
}

fn is_valid_public_key(public_key: Span<felt252>) -> bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 3 — Medium: is_valid_public_key accepts keys that brick the account at deploy.

Only the 29-felt canonical base-Q packing is checked, so (a) a standard coefficient-domain Falcon h (instead of the NTT-domain h_ntt the verifiers require) and (b) the all-zero key both deploy successfully and can never validate a signature; recovery via set_public_key is impossible since it requires a self-call. The NTT-domain requirement appears only in the doc comment at account.cairo:9, not in the README or preset constructor docs.

Suggested fix: state the NTT-domain requirement in packages/account/README.md and both preset constructors; consider rejecting the all-zero key here.

snforge test -p openzeppelin_account --release --features falcon_fast_tests falcon_512 --max-n-steps 100000000
snforge test -p openzeppelin_presets --release --features falcon_presets_tests falcon_512 --max-n-steps 100000000

- name: Build release-profile presets

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 3 — Medium: CI never compiles the presets to CASM, and the default profile silently produces an undeclarable class.

With casm = false in packages/presets/Scarb.toml, this step only ever produces Sierra — nothing verifies the class lowers to CASM or fits the 81,920-felt bytecode cap. Verified on this branch:

  • release profile: Falcon512ShakeAccountUpgradeable = 57,120 CASM felts (69.7% of the cap), direct = 57,048; 2.16 MB Sierra vs the ~4.09 MB class-size cap.
  • dev profile: plain scarb build -p openzeppelin_presets emits a 32.6 MB contract_class.json (8× over the declare limit, debug info balloons), and with casm = true it hard-fails with OffsetOverflowinlining-strategy = "avoid" still honors #[inline(always)] on the 9,480-line NTT body.

A future regression lands with green CI, and headroom is ~24.8k felts.

Suggested fix: enable CASM for the release target and assert bytecode size < 81,920 in CI; document that these presets are release-profile-only; consider recording them in scripts/benchmarking.

### Interfaces

- [`ISRC6`](https://docs.openzeppelin.com/contracts-cairo/3.x/api/account#ISRC6)
- [`IFeltArrayDeployable`](https://docs.openzeppelin.com/contracts-cairo/3.x/api/account#IFeltArrayDeployable)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 2 — Low: broken doc links and misplaced maintainer content.

  • The anchors here (#Falcon512AccountComponent, #IFeltArrayDeployable, …, and #Falcon512ShakeAccountUpgradeable in packages/presets/README.md) target adoc pages that were added in 96110a6 and deleted in 53aa1ee — they resolve nowhere. The URLs also hardcode /contracts-cairo/3.x/ while the package is 4.0.0-alpha.1.
  • The "regenerate the NTT sources" section (lines 48–57) is maintainer-only and references scripts/falcon_512/, which isn't in the published package — better in CONTRIBUTING.md.
  • Also missing from any README: the new presets aren't in sncast_scripts/src/declare_presets.cairo / its build-external-contracts, so they won't be pre-declared on Sepolia as the presets docs promise.

run: scarb fmt --check --workspace

- name: Run tests
run: snforge test --workspace --features fuzzing --fuzzer-runs 200

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 2 — Low: CI runs the expensive Falcon suite redundantly.

The 65 dev-profile Falcon tests run twice (here and in the coverage run at line 67), several above 1e9 L2 gas, serial in one job — and the coverage run doesn't enable falcon_presets_tests, so the Falcon presets (SRC9 routing, upgrade's assert_only_self) report as entirely untested in Codecov anyway. Two further cost sinks: test_fast_ntt_matches_generic_reference_for_every_basis_vector alone costs 7.65B gas (~76M steps) and is single-handedly why the 100M step cap is needed (the boundary/pseudorandom test already covers the interesting cases — sample every 16th basis vector or feature-gate it); and the #[cfg(test)] felt wrapper re-inlines the 31k-statement NTT body, making ~26.7% of the test program duplicate code and a 44 MB test artifact (have it call ntt_falcon512_fast_u16_unchecked and convert instead).

msg(), key.span(), with_appended(valid_signature.span(), 0).span(),
),
);
assert!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 2 — Low: several rejection tests don't actually pin the guards they target.

  • Non-canonical-slot and out-of-range-salt rejection is asserted by replacing a slot wholesale, which also changes the coefficients/hash — the assertion still passes if the guard is deleted. Tamper additively (signature[0] + Q_POW_9) instead, and probe indices 28 and 59, not just 0 (the last-slot guards in packing.cairo:122-125 never execute through verify).
  • No component_state_for_testing() block (unlike test_account.cairo), leaving unreachable: initializer called twice, the stored_len == new_public_key.len() branch at account.cairo:397, read_public_key on empty storage, and set_public_key with a non-canonical correct-length key.
  • test_repeated_key_rotation_keeps_exact_storage_length rotates twice to the same key and asserts only the length — rotate to a distinct third key and compare full content.
  • In-range tampering is tested for salt_a but never salt_b; SRC9 replay is checked only via the nonce flag, never by resubmitting.

}

/// Verifier for the 60-felt SHAKE-256 signature carrying a polynomial-product hint.
pub impl Falcon512ShakeVerifier of Falcon512SignatureVerifier {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 1 — Info: cross-variant signature malleability.

Since the hint layout is s1(29) || salt(2) || mul_hint(29), truncating a 60-felt hint signature to its first 31 felts yields a valid direct signature for the same key and message hash (the preset tests rely on this via copy_prefix). Not exploitable for replay — the tx hash and SNIP-12 domain bind the account address — but integrations that deduplicate or key state off exact signature bytes would see two distinct byte strings authenticating the same message. Worth one sentence in the README.

let two16: NonZero<u64> = 0x10000_u64.try_into().unwrap();
let b256: NonZero<u64> = 0x100_u64.try_into().unwrap();
let mut coeffs: Array<u16> = array![];
loop {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Severity 1 — Info: the squeeze loop has no iteration cap and over-runs after completion.

Termination is probabilistic (an attacker can't cheaply steer SHAKE output, so not exploitable), and once 512 coefficients are collected the loop still walks the remaining lanes of the current block (~60 wasted candidate evaluations) while push_candidate re-checks coeffs.len() != 512 per candidate (~580×). A hard block cap plus breaking out of the lane loop on completion would bound the worst case explicitly and shave a little gas.

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.

[Task]: Include Onchain PQ Components

3 participants