Skip to content

Airdrop snapshot matching and claim proofs in the SDK - #653

Merged
n13 merged 10 commits into
mainfrom
illuzen/airdrop-claim
Sep 15, 2026
Merged

n13 merged 10 commits into
mainfrom
illuzen/airdrop-claim

Conversation

@illuzen

@illuzen illuzen commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • findAirdropMatches: given the claim-server snapshot address list, derives every historical address format locally (three Dilithium hash eras and five wormhole sponge schemes, HD scan of m/44'/189189189'/0'/{0,1}'/{0..=16}' plus explicit secrets) and returns which rows belong to this wallet. Keys and secrets never leave the process.
  • buildAirdropDilithiumClaim: signs address || claim_account || expiry under the qp-airdrop-claim-v1 FIPS 204 context (ML-DSA-87 only) and returns the POST /claim body fields.
  • proveAirdropWormhole: generates the rate-8 ownership proof in-process (async binding; the circuit builds on a Rust worker thread).
  • Ownership crates come from the qp-zk-circuits v4.4.0 git tag as a separate source, so the existing wormhole 4.3.0 crates.io stack is unchanged.

Test plan

  • 9 Rust unit tests: golden vectors for the historical sponges, HD scan end-to-end, snapshot matching, claim signature round-trip incl. context mismatch, foreign-prefix rejection
  • wormhole_proof_generates (ignored by default) passes with cargo test --release -- --ignored
  • flutter analyze clean on quantus_sdk
  • End-to-end against a local claim server (airdrop-claim repo, data/dev_dummy.csv)

Made with Cursor

illuzen and others added 3 commits September 12, 2026 16:18
find_airdrop_matches derives every historical Dilithium hash and wormhole
sponge scheme locally and intersects with the claim-server snapshot, so the
app can show which rewards belong to this wallet without sending keys
anywhere.

Co-authored-by: Cursor <cursoragent@cursor.com>
build_airdrop_dilithium_claim signs address || claim_account || expiry under
the qp-airdrop-claim-v1 context; prove_airdrop_wormhole generates the rate-8
ownership proof in-process. Ownership crates come from the qp-zk-circuits
v4.4.0 tag so the existing wormhole 4.3.0 stack is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen illuzen added the bot-review Request automated review from review-bot label Sep 12, 2026

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

Reviewer model: GPT-6 Astra

REQUEST_CHANGES — reviewed 90f6517aee84f2d66d289de180eb6984fd532351 against base e843b06b49e4c208f7b4a8c603a91f4578780e96.

  1. [P1] Use the historical permutation for v0.9 addressesairdrop.rs:90–91. hash_no_pad_v09 uses the current Poseidon2::new(), but the permutation constants changed too. The original v0.9.5 implementation generated its constants with ChaCha8 and seed 0x189189189189189; the current permutation uses the later constants. I compiled the original tagged implementation and compared it with this PR's reconstruction: hash_padded([0]) should be b17b423096da9ebd57af5038b490257d9c492e64059c0ccff23f44e6293213d4, but returns 06dfabf1fc7893bbc9443799adffd3b95bf56d0e3f9712508f8da7cc07aecee2. A 2592-byte input also differs. Consequently, actual v0.9 Dilithium and wormhole snapshot addresses are missed, and their Dilithium claims fail the local address-ownership check. Preserve the original permutation as well as its sponge wrapper, and add independent legacy vectors; the current tests only pin the current scheme.

  2. [P2] Export the new APIs through the public SDK libraryairdrop.dart:22. quantus_sdk.dart exports the existing crypto, UR, and wormhole APIs but never exports this new module. A consumer importing package:quantus_sdk/quantus_sdk.dart gets undefined-name errors for all three new functions; I reproduced this with a compile test. Add the airdrop export and a public-import smoke test so callers can use the feature without importing implementation files under src/.

  3. [P2] Apply the repository's Dart formatting after regeneration — the eight changed Dart files fail the configured 120-column formatter. This reproduces locally and is the failure in the PR's Analyze job, which stops before analysis/tests. Run the repository formatting command and include its output.

Validation:

  • git diff --check: passed; Dart/Rust generated content hashes agree.
  • cargo test --locked --lib: 63 passed, 1 ignored.
  • cargo test --locked --release --lib wormhole_proof_generates -- --ignored: passed.
  • SDK flutter test --no-pub --exclude-tags=native: 480 passed.
  • Original v0.9.5 comparison: confirmed mismatches for 1-, 32-, and 2592-byte inputs.
  • Public-import compile test: fails for the three missing exports.
  • Workspace formatting check: fails only on the eight changed Dart files.
  • Workspace analysis was stopped at the requested 10-second limit; full analysis and claim-server HTTP integration remain unverified.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 12, 2026
The v0.9.x qp-poseidon permutation used different round constants
(ChaCha8, seed 0x189189189189189) than the current one, so reconstructing
its sponge on the current permutation produced wrong addresses. Depend on
the real qp-poseidon-core 0.9.5 crate for the v09 schemes and pin them
with vectors from the original implementation.

Also export the airdrop API from quantus_sdk.dart with a public-import
smoke test, and format the regenerated Dart files at 120 columns.

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen illuzen added the bot-review Request automated review from review-bot label Sep 14, 2026

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

Reviewer model: GPT 5.6 Sol

REQUEST_CHANGES — the v0.9 permutation, public export, and formatting blockers from the prior review are fixed at e0337779df4259501c7a5ff5d7036f0f9578bd56, but the historical matcher still omits two shipped Resonance address families.

  1. [P1] Include the pre-0.9.5 Resonance Dilithium schemesairdrop.rs:208-219 starts DILITHIUM_SCHEMES at qp-poseidon-core 0.9.5. However, shipped chain tag v0.0.12-resonance-alpha pins poseidon-resonance 0.8.0, whose account derivation uses legacy Plonky2 Poseidon over 8-byte limbs padded to 73 field elements. A subsequent shipped Resonance revision, e9fc9b9, pins qp-poseidon 0.9.1, which uses legacy Plonky2 Poseidon over 4-byte limbs padded to 188 field elements. Both AccountId hashes differ from 0.9.5's Poseidon2/length-prefixed scheme and from the two later schemes listed here; historical golden vectors for the same public key confirm distinct addresses. The companion reward snapshot currently contains 50 positive Resonance-labelled rows, so an affected early miner receives no AirdropMatch and cannot build a claim. Add both exact derivations with golden public-key/address vectors, and add/accept the same scheme IDs in airdrop-claim; changing only this SDK would still be rejected by the server.

Validation at the exact reviewed head:

  • git diff --check, Rust formatting, and Dart formatting passed.
  • cargo test --locked --lib: 65 passed, 1 ignored.
  • Ignored release-mode wormhole_proof_generates: passed.
  • flutter test --exclude-tags=native: 481 passed.
  • flutter analyze . --fatal-infos: no issues (4.5 seconds).
  • Strict Clippy reached only an unchanged pre-existing needless_range_loop in wormhole.rs:376; no PR-specific Clippy error was observed.
  • GitHub Analyze and dependency-cooldown checks are green.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 14, 2026
Shipped Resonance chains predate qp-poseidon 0.9.5: chain tag
v0.0.12-resonance-alpha pinned poseidon-resonance 0.8.0 (legacy plonky2
Poseidon, 8-byte limbs zero-padded to 73 felts) and chain rev e9fc9b9
pinned qp-poseidon 0.9.1 (same permutation, 4-byte limbs padded to 188).
Early miners' snapshot addresses use these hashes, so match and claim
under dilithium-v08-padded and dilithium-v091-padded, which the claim
server now also accepts.

The legacy Poseidon round constants are unchanged in today's qp-plonky2
(verified against the fork revs both chains pinned), so both schemes
hash through the crate already in the tree; golden vectors were
generated with the exact historical crates.

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen

illuzen commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the pre-0.9.5 Resonance schemes in 7057e96.

  • Added dilithium-v08-padded (poseidon-resonance 0.8.0: legacy plonky2 Poseidon, 8-byte LE limbs zero-padded to 73 felts) and dilithium-v091-padded (qp-poseidon 0.9.1: 4-byte limbs padded to 188 felts) to the matcher.
  • Golden vectors were generated from the exact shipped sources: poseidon-resonance at rev fcb49a7 with the plonky2 fork at rev 80a1000 (both pinned by chain tag v0.0.12-resonance-alpha's Cargo.lock), and crates.io qp-poseidon 0.9.1 (pinned by chain rev e9fc9b9). Vectors cover 1-, 32-, and 2592-byte (ML-DSA-87 pubkey sized) inputs for both schemes.
  • The legacy Poseidon round constants are byte-identical between those historical revs and the qp-plonky2 1.5.5 already in the SDK's tree (the vectors pin this), so no new dependency was needed.
  • The claim server accepts the same scheme ids as of Quantus-Network/airdrop-claim#1 commit d06b9c6, and quantus-cli got the same matcher parity in Add airdrop check and claim commands quantus-cli#163 commit 7892378.

cargo test --locked --lib: 68 passed, 1 ignored; clippy clean on the changed file; no Dart regeneration needed since the FFI surface is unchanged.

@illuzen
illuzen requested a review from n13 September 14, 2026 06:13
@illuzen illuzen added the bot-review Request automated review from review-bot label Sep 14, 2026

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

Reviewer model: GPT 5.6 Sol

APPROVE — the prior historical-derivation, public-export, and formatting blockers are resolved at 7057e96de4cecfef80d714738cf289ed14b09ec0; I found no blocking issues.

The two added pre-0.9.5 Resonance schemes match the shipped sources: poseidon-resonance 0.8.0 uses 8-byte little-endian limbs with a 73-felt minimum, while qp-poseidon 0.9.1 uses 4-byte limbs with a 188-felt minimum. Their golden vectors pass, and the companion claim-server head accepts the same scheme IDs. The generated Dart/Rust bridge hashes agree, the APIs are publicly exported, the signature is bound to the address, payout account, expiry, and FIPS 204 context, and the ownership proof binds the payout through public inputs.

Validation:

  • git diff --check and Rust/Dart formatting checks passed.
  • cargo test --locked --lib: 68 passed, 1 intentionally ignored.
  • cargo test --locked --release --lib wormhole_proof_generates -- --ignored: passed.
  • flutter test --exclude-tags=native in quantus_sdk: 481 passed.
  • Pinned Melos SDK analysis with fatal infos: no issues in 2.7 seconds.
  • cargo +nightly check --locked --target aarch64-apple-ios -Zbuild-std=std,panic_abort --lib: passed.
  • Strict Clippy reaches only the unchanged pre-existing src/api/wormhole.rs:376 needless_range_loop; allowing that single base warning leaves all targets clean.
  • GitHub Analyze and dependency-cooldown checks are green.

The unchecked item remains a live HTTP round trip against the separate claim-server PR; I inspected its current request schema, scheme registry, and ownership-verifier path and found them consistent with this SDK.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 14, 2026
illuzen and others added 3 commits September 14, 2026 16:20
The middle HD path component is not always a change branch: the app uses
0 (external) and 1 (dedicated change branch since July 2026), but the
CLI's 'wormhole multiround' uses it as a round counter, deriving
m/44'/189189189'/0'/round'/index' with a default of 2 rounds. Scanning
only {0,1} missed every multiround address from round 2 on. Scan
branches/rounds 0..=8 for each index, and stretch the BIP39 seed once
instead of per path, which also makes the wider scan faster and zeroizes
the mnemonic after stretching.

Co-authored-by: Cursor <cursoragent@cursor.com>
The ownership crates are on crates.io as of the v4.4.0 release, so the
git-tag workaround is gone. Registry semver unification pulls the
qp-wormhole-* family to 4.4.0; the wormhole circuit sources are
identical between 4.3.0 and 4.4.0 (only additive config and aggregator
prover reuse), so proofs stay chain-compatible.

Co-authored-by: Cursor <cursoragent@cursor.com>
Port the quantus-cli secret-hygiene fixes: preimages are built in
pre-sized zeroize-on-drop SensitiveFelts buffers; the v0.9.5 sponge is
rebuilt locally from public crates and hashes secrets from borrowed
slices (the historical crate's hash_no_pad consumes and frees its Vec
unscrubbed); the rate-4 sponge wipes its stack state; the prover
boundary uses Secret::new and scrubs the FFI-owned Vec; the HD scan
buffer is wiped before it is freed. An allocator-based regression test
scans every freed block for the secret in raw-byte and felt encodings
with no exemptions; golden vectors pin the local sponge to the
historical hasher.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

There's a few times where we changed the public key

I am making a list but this will have to be in here.

@n13

n13 commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

List is here

When the ML-DSA-87 public key for a given mnemonic changed

#: 1
Library change: —
Adopted by (PR, date): apps #57 2025-07-17 (Multiple accounts); CLI #41 2025-10-06 (derivation path)
What changed: Wallet accounts moved from non-HD Keypair::generate(seed64) to HD at m/44'/189189'/N'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/0/0 —
last two levels non-hardened, BIP32 with master HMAC key "Bitcoin seed"
Why: Multiple-accounts feature; CLI followed the app
────────────────────────────────────────
#: 2
Library change: qp-rusty-crystals #36 nist_verification, merged 2025-11-04, released dilithium-v2.0.0 +
hdwallet-v1.0.0 2025-11-06
Adopted by (PR, date): chain #296 2025-11-07; apps #284 2025-11-07; CLI #46 2025-11-09
What changed: Keygen seed expansion SHAKE256(seed[..32]) → FIPS 204 SHAKE256(seed ‖ K ‖ L), absorbing the
whole input. Side effect: the non-HD account went from an effective 32-byte input to the full 64-byte
BIP39 seed (hdwallet-v1.0.0 generate_keys = Keypair::generate(&self.seed))
Why: NIST KAT conformance (old keygen did not match FIPS 204 vectors)
────────────────────────────────────────
#: 3
Library change: hdwallet #48 Neodyme audit fixes 2025-12-13 removed generate_keys
Adopted by (PR, date): CLI only: illuzen/new-plonky2 → v1.0.0 #63 2026-02-17 (commits 70b8342, 3f71714)
What changed: Default path hardened …/0'/0'/0'; --no-derivation wallets became BIP32 child at
m/44'/189189'/0' instead of raw seed
Why: Fallout of the audit API removal
────────────────────────────────────────
#: 4
Library change: qp-rusty-crystals #60 "Remove k256" 2026-03-06, released hdwallet-v2.1.0
Adopted by (PR, date): chain #452 2026-03-28; CLI #76 "Fix keygen" 2026-04-01; apps #449 Planck
2026-04-14
What changed: BIP32 master key "Bitcoin seed" → "Dilithium seed", non-hardened children removed → every
HD entropy changed (Dilithium and wormhole). Same time, app + CLI aligned the non-HD "legacy" account
with chain from_seed: FIPS keygen over seed64[..32]
Why: Drop secp256k1 dependency; sync keygen across chain/CLI/app

…Bitcoin-seed tree

The address-hash schemes covered how a public key became an AccountId, but
the ML-DSA-87 public key for the same mnemonic changed four times:

1. pre Nov 2025: SHAKE256(seed[..32]) expansion, non-HD or soft HD paths
   (m/44'/189189'/N'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/0/0) under the "Bitcoin seed" BIP32 master
2. Nov 2025 (dilithium 2.0.0): FIPS 204 expansion absorbing the whole seed
3. Feb 2026 (CLI): hardened default path and --no-derivation account child
4. Mar 2026 (hdwallet 2.1.0): "Dilithium seed" master, hardened-only —
   which also changed every wormhole HD entropy

find_airdrop_matches now derives candidate keypairs for every era (both
seed expansions are rebuilt locally from the current crate's public
primitives, in wipeable buffers — the historical crates' keygens free
seed-bearing heap copies unscrubbed) and scans wormhole paths under both
BIP32 masters plus the legacy master-node secret. Matches carry a
dilithium_keygen id; the new build_airdrop_dilithium_claim_from_mnemonic
re-derives that era's key and signs with the current crate.

Golden vectors are pinned against the shipped dilithium 1.0.3 / 2.0.0 and
hdwallet 1.0.0 crates (pk and sk sha256 per era, BIP32 entropies).

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen

illuzen commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 152297a (and mirrored in quantus-cli in Quantus-Network/quantus-cli#163, commit 8cbd49c): the matcher now derives candidate ML-DSA-87 keypairs for every historical keygen era, not just historical address hashes of the current key.

Keygen eras covered (each candidate public key is hashed under all five address schemes):

  1. pre Nov 2025 — pre-FIPS expansion SHAKE256(seed[..32]), non-HD and soft HD paths m/44'/189189'/N'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/0/0 under the "Bitcoin seed" BIP32 master
  2. Nov 2025 (dilithium 2.0.0) — FIPS 204 expansion over the whole input (the non-HD account silently moved from a 32-byte to the full 64-byte seed input; both are scanned)
  3. Feb 2026 (CLI) — hardened default path .../N'/0'/0' and --no-derivation account child m/44'/189189'/N'
  4. Mar 2026 (hdwallet 2.1.0) — "Dilithium seed" master and the seed64[..32] legacy account (current scheme)

HD families scan account indexes 0–8. Since the era-4 change also moved wormhole HD entropy to the new master, the wormhole scan now additionally walks the same path grid under the "Bitcoin seed" master, plus the legacy master-node secret (generate_wormhole_pair used m directly).

Implementation notes:

  • The two historical seed expansions are rebuilt locally from the current dilithium crate's public primitives (fips202/poly/polyvec/packing) in stack buffers that are wiped — the shipped 1.0.3/2.0.0 keygens copy the seed into heap Vecs they free unscrubbed, so they never see the seed. "Bitcoin seed" BIP32 goes through nam-tiny-hderive (the exact crate hdwallet <2.1.0 wrapped), which zeroizes its key material.
  • Byte-equality with the shipped crates is pinned by golden vectors generated by running the actual dilithium 1.0.3 / 2.0.0 and hdwallet 1.0.0 crates (sha256 of pk and sk per era, plus BIP32 entropies).
  • Matches carry a dilithiumKeygen id; the new buildAirdropDilithiumClaimFromMnemonic re-derives that era's keypair and signs with the current crate (historical sk bytes load cleanly into 4.1.1's validated SecretKey::from_bytes; sign/verify roundtrip is covered by tests, so server-side verification is unaffected).
  • Dedupe keeps the wallet-pubkey-based match when the current key also matches, so the existing claim path stays mnemonic-free.

@n13 n13 added the bot-review Request automated review from review-bot label Sep 14, 2026

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

Reviewer model: GPT 5.6 Sol

REQUEST_CHANGES — reviewed 152297a761f32fd6756e10dc64cf0b1697f0f5bb against base e843b06b49e4c208f7b4a8c603a91f4578780e96; two blocking issues remain.

  1. [P1] Make the scan buffer zeroizing across reallocations and errorsairdrop.rs:710. secrets starts as a zero-capacity Vec<([u8; 32], String)> and receives secrets at lines 753, 774, and 782. Every capacity growth copies the inline secret arrays into a new allocation and frees the old allocation without wiping it; the loop at lines 807–812 only scrubs the final allocation. I reproduced this through the public find_airdrop_matches API with nine known extra secrets and the same deallocation scanner: a freed 448-byte block still contained the secret pattern. Early returns have the same problem—for example, an invalid later extra secret returns at line 781 and drops all earlier entries before the cleanup loop. Pre-reserve the full count before writing secrets and give the collection RAII cleanup so success, error, and panic paths all wipe; extend the allocator regression to call find_airdrop_matches itself on both success and error paths.

  2. [P1] Do not truncate valid historical HD accounts at index 8airdrop.rs:463. dilithium_keygen_ids only emits accounts 0..9, but the shipped multiple-account wallet had no such cap: getNextFreeAccountIndex returned maxIndex + 1, and the UI continued exposing account creation. Consequently, a user who mined to the tenth account (.../9'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/0/0) gets no historical key candidate, no AirdropMatch, and no keygen id to build the claim. The same fixed-bound problem exists for wormhole receive/change indices above 16. Accept the wallet's actual stored indices/paths (or an explicit caller-supplied discovery bound) instead of silently hard-coding a smaller range, and test the first previously excluded index.

Validation:

  • git diff --check, Rust formatting, and changed-file Dart formatting passed.
  • cargo test --locked --lib: 73 passed, 1 intentionally ignored.
  • cargo test --locked --release --lib wormhole_proof_generates -- --ignored: passed.
  • flutter test --exclude-tags=native in quantus_sdk: 481 passed.
  • Pinned Melos analysis reported no issues for cold-wallet, miner, and quantus_sdk; it was stopped at the required 10-second cap while mobile-app was running. GitHub Analyze is green at this head.
  • Strict Clippy passes with the single unchanged src/api/wormhole.rs:376 warning allowed; without that allowance it fails only on that pre-existing warning.
  • GitHub dependency cooldown is red because the 4.4.0 crates are 4–5 days old, below the repository's 30-day policy.

The companion claim-server head accepts the five Dilithium scheme ids and its claim/proof bindings match this SDK. A live HTTP round trip remains unverified.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 14, 2026
… by RAII

- DILITHIUM_SCAN_ACCOUNTS and HD_SCAN_INDEXES go from 9 / 17 to 100. The
  shipped multiple-accounts wallet never capped the account index, so a
  miner on account 9 (or a wormhole address at index 17) got no match and
  no keygen id. The new test pins the first previously excluded index and
  the last index in range for both.
- New crate-root `sensitive` module: one `Wipe` trait and `SensitiveVec`
  replace SensitiveFelts, SecretKeyBytes and the manual wipe loops. The
  wormhole scan buffer is pre-sized for both HD trees plus the provided
  secrets and wipes on drop, so growth, early error returns and panics no
  longer free secret-bearing blocks. The FFI-provided extra secrets are
  wrapped the same way, so an invalid length no longer leaves the later
  inputs unwiped.
- The allocator regression test now drives find_airdrop_matches on the
  success path (nine secrets, forcing reallocation) and the error path.
- Regenerated the Dart doc comment for the new scan range.
@n13

n13 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Addressed both P1s from the last review in 2ddd6e3.

HD scan boundsDILITHIUM_SCAN_ACCOUNTS and HD_SCAN_INDEXES go from 9 / 17 to 100 (branches stay at 9). hd_scan_reaches_high_account_and_address_indexes puts Dilithium accounts 9 and 99 and wormhole indexes 17 and 99 in one snapshot and asserts all four match with the expected keygen id / path. Cost of the full mnemonic scan (503 ML-DSA-87 keygens × 5 address schemes, 1801 wormhole secrets × 5 schemes) is ~0.3 s in release on an M-series Mac.

Scan buffer zeroization — new crate-root sensitive.rs with one Wipe trait and SensitiveVec<T> (pre-sized, Deref<[T]>, wipes on drop). It replaces SensitiveFelts, SecretKeyBytes, the wipe_bytes/wipe_felts pair and the manual end-of-function wipe loop. In find_airdrop_matches:

  • secrets is reserved up front for both HD trees + the legacy master node + the provided secrets, so no reallocation ever frees a secret-bearing block, and every return path (including the ?s inside the HD loops) wipes it.
  • extra_wormhole_secrets is wrapped in the same type, so an invalid length at index i no longer drops the unvisited inputs i+1.. unscrubbed (the previous per-element wipe only covered visited entries).

The allocator regression now also drives find_airdrop_matches itself: success path with nine copies of the pattern (forces several reallocations under the old code) and the error path (good secret, bad length, unvisited good secret). I checked the test's power by simulating both old behaviours: a growing Vec fails it with an 896-byte freed block, and unwrapped extra secrets fail it with a 32-byte block.

The module lives outside api/ because FRB otherwise emits an abstract class Wipe (even with #[frb(ignore)]), and the impls are generic ([T; N], Vec<T>, (T, String)) because a concrete [u8; 32] in any impl header makes FRB hoist U8Array32 into a new lib.dart. Regenerated bindings differ only in the airdrop doc comment.

Validation: cargo fmt --check, clippy (only the pre-existing wormhole.rs:376 warning), cargo test --locked --lib 74 passed / 1 ignored in debug and release, wormhole_proof_generates --ignored in release, flutter test test/public_exports_test.dart, git diff --check.

Note the PR summary still says {0..=16}'; the scan is now {0..=8}'/{0..=99}'.

@n13 n13 added the bot-review Request automated review from review-bot label Sep 15, 2026

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

Reviewer model: GPT 5.6 Sol

REQUEST_CHANGES — reviewed 2ddd6e303c50659092a46d610a7faea3277661f8 against base e843b06b49e4c208f7b4a8c603a91f4578780e96; the scan-buffer RAII fix is sound, but two blocking issues remain.

  1. [P1] Do not pass the wallet seed through the old non-zeroizing BIP32 implementationairdrop.rs:472-477 and airdrop.rs:712-716 feed the 64-byte BIP39 seed into nam-tiny-hderive 0.3.1's ExtendedPrivKey::derive for every historical candidate. That exact implementation uses hmac 0.12's consuming Hmac<Sha512> and returns/moves extended keys by value. Its HMAC state/output has no zeroizing drop, so wiping only the returned entropy cannot erase the raw seed, parent secret, chain-code pad images, and SHA/HMAC state left in dead stack frames. This is the same implementation flaw documented by the current HD-wallet crate's optimized stack-residue regression, whose replacement uses wiped buffers and in-place derivation. Reimplement the legacy "Bitcoin seed" derivation with those protections (while retaining the historical soft-child behavior), or use a patched dependency, and cover it with an optimized stack-residue test.

  2. [P1] Replacing the old scan cutoff with 100 still silently excludes valid wallet pathsairdrop.rs:35-41 and airdrop.rs:408-411 now scan 0..100, so account/address index 100 is simply the next false-negative. Transparent account allocation has no maximum, and wormhole change allocation likewise increments without a cap. A supported wallet at index 100 therefore still receives “no match” and cannot construct its claim. Accept the wallet's actual paths or caller-supplied discovery bounds (and surface when a bounded scan is incomplete) instead of silently treating a heuristic ceiling as exhaustive; cover the first excluded index.

Validation:

  • git diff --check, Rust formatting, and changed-file Dart formatting passed.
  • cargo test --locked --lib: 74 passed, 1 intentionally ignored.
  • The ignored release-mode wormhole_proof_generates test passed.
  • Optimized scan-buffer zeroization and high-index regression tests passed.
  • Strict Clippy passed with only the unchanged wormhole.rs:376 lint allowed.
  • flutter test --no-pub test/public_exports_test.dart: passed.
  • Workspace analysis cleared cold-wallet and miner before being stopped at the required 10-second cap; GitHub's full Analyze job is green.
  • The companion claim-server PR head accepts the same five Dilithium schemes and uses the same signed/proved fields.
  • GitHub dependency cooldown remains red because the 4.4.0 ownership/wormhole crates are 4–5 days old versus the 30-day policy; it requires the normal wait or emergency-bypass process.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 15, 2026
@n13

n13 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Ok these are not valid concerns - GTG

@n13
n13 merged commit ad8749f into main Sep 15, 2026
1 of 4 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.

2 participants