Skip to content

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

Merged
ericnordelo merged 9 commits into
mainfrom
feat/add-post-quantum-accounts
Sep 15, 2026
Merged

ericnordelo merged 9 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: Essentials

Run ID: 81375c49-45af-4455-9199-13e9ae6ee69b

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
🚥 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

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.

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)

Contract Old New Δ Note
ERC6909TokenSupplyMock 6042 6628 🟢 +586
GovernorMock 19969 19945 🔴 −24
GovernorQuorumFractionMock 24705 24681 🔴 −24
GovernorTimelockedMock 24086 24062 🔴 −24
SRC9AccountMock 5322 5442 🟢 +120

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

Contract Old New Δ Note
ERC6909TokenSupplyMock 110003 119664 🟢 +9661
GovernorMock 476955 476299 🔴 −656
GovernorQuorumFractionMock 576289 575733 🔴 −556
GovernorTimelockedMock 574770 574247 🔴 −523
SRC9AccountMock 121871 126225 🟢 +4354

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

@codecov

codecov Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.67249% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ages/account/src/falcon_512/hashing/shake256.cairo 98.46% 2 Missing ⚠️
...account/src/falcon_512/hashing/hash_to_point.cairo 98.55% 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-82 — assert(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,71 — pub 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,213 — pub 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

@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.

Comment thread .github/workflows/test.yml Resolved
Comment thread packages/account/README.md Resolved
Comment thread .github/workflows/test.yml Resolved
@immrsd
immrsd self-requested a review September 9, 2026 11:21

@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.

Re-reviewed after the updates — the responses are substantial and they verify cleanly on my side: the regenerated NTT sources match byte-for-byte, the resource and gas budgets actually fail when tightened, and check_fixtures.py independently reproduces all four signatures. Remaining items are in the inline comments; the one I'd still consider blocking before approval is the upgrade-compatibility warning (existing thread on the preset).

One request: could the lint-related fixes move to their own small PR? That's the ERC6909TokenSupplyMock hooks change with its new supply tests, the ERC721Consecutive DefaultConfig imports, the governor unwrap_or_else changes, and the SRC9 mock assert. They're all legitimate — each traces to a real scarb lint warning, and the ERC6909 one is a genuine mock fix prescribed by the new with_components diagnostic — but main currently carries the identical warnings, so a standalone PR would fix them where they live, and a behavioral change to a shared mock is easy to miss inside a Falcon feature PR.

Related follow-up worth considering: the CI "Cairo lint" step only runs scarb fmt --check, so these diagnostics (including the new ERC6909 hook-wiring one) aren't enforced anywhere. Promoting it to also run scarb lint would keep this from recurring.


// Budgets include deployment and test setup as well as one validation call.
#[test]
#[available_gas(l2_gas: 75000000)]

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.

Medium: the budgets don't assert the number that matters.

These bundle declare + deploy + setup + validate into one #[available_gas], so the headroom they demonstrate is against a repo-chosen number, not the protocol limit — validate_max_sierra_gas (~1e8) and validate_max_n_steps (1e6) are asserted nowhere in the repo. If a compiler or gas-schedule change pushes a single __validate__ past the cap, deployed accounts of that variant can no longer validate any transaction, and CI stays green.

Suggestion: one test per variant that isolates a single __validate__ (or subtracts the measured deploy baseline) and asserts it under the protocol cap with an explicit margin.

MAX_BYTECODE_FELTS = 81_920
MAX_CLASS_BYTES = 4_089_446
# Repository regression budgets leave room below the protocol limits.
BUDGETS = {"sierra_felts": 60_000, "casm_felts": 50_000, "artifact_bytes": 2_000_000}

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.

Three nits:

  • The protocol-cap checks below can never fire — the repo budgets are all strictly tighter and raise first. Fine as a backstop, but worth a comment saying only the budgets are enforced.
  • The "including debug info and whitespace makes this stricter" claim doesn't hold: stripping sierra_program_debug_info from the release artifact changes the size by ~86 bytes.
  • The tightest gate is the least meaningful one: artifact bytes sit at ~89% of the 2MB budget while CASM is at ~43% of its cap, so the first alarm will likely be a false one. Consider rebalancing the byte budget.


// Production inputs come from canonical base-Q unpacking; the test wrapper documents the same
// coefficient precondition. The generated unchecked NTT is sound for both call paths.
let s1_ntt = forward_ntt(s1);

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.

Low: the norm check could run before these two transforms.

The norm accumulation depends only on msg_point, mul_hint, and s1 — none of the NTT outputs. Moving it ahead of the forward_ntt calls makes every norm-failing signature (wrong key, replayed/corrupted signature) reject before paying for both transforms. Same idea for the direct variant: Σ center_sq(s1) > β² can reject before its NTT + INTT.

Caveat: this only cheapens the honest-failure path — crafted inputs (e.g. s1 = 0, mul_hint = msg_point) still pass the norm gate and force the transforms — but it's a free win.

}

/// Inverse-transform configuration without unused forward root tables.
pub fn inverse_config() -> NttConfig {

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.

Info: inverse_config() returns a config with empty merge_roots; passing it to ntt() panics with a bare index-out-of-bounds. Internal-only, but a latent footgun — an assert with a clear message at the top of ntt() (or separate forward/inverse config types) would make the misuse obvious.


N = 512
PRIME = 2**251 + 17 * 2**192 + 1
NORM_BOUND = 34_034_726

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.

NORM_BOUND, Q, and the felt layout are duplicated here with no cross-check against the Cairo constants (SIG_BOUND_512, PUBLIC_KEY_FELTS, ...) — if either side changes, the checker silently drifts. Cheap fix: read the constants out of the Cairo source and compare, the way the NTT generator check does.

Also worth one line in CONTRIBUTING: the fixtures are verifiable but not reproducible (no seeds or signer committed), so a layout change can only be re-checked, never re-derived. Committing the signer script or the seeds would close that.

resource limits again. A successful local build does not verify a public network's supported
Sierra version or permitted libfuncs.

### Validation resources

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.

Three additions worth making in this section:

  • The figures come from a single reference vector; ~63% of signatures need one extra Keccak permutation (rejection sampling), so label them typical rather than worst-case — direct sits fairly close to the validate cap.
  • A transaction that fails __validate__ costs the sender nothing but costs the sequencer the full ~46-56M gas, roughly 100x a STARK-curve account. Worth stating that the mitigation is sequencer-side mempool policy, not the contract.
  • Since both variants ship, a sentence on when to pick hint vs direct (hint is currently ~10M gas cheaper; direct has 31-felt signatures) so the choice isn't guesswork.

@ericnordelo
ericnordelo merged commit 8c8bb47 into main Sep 15, 2026
15 checks passed
@ericnordelo
ericnordelo deleted the feat/add-post-quantum-accounts branch September 15, 2026 13:00
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