Problem
scan_notes_for_foreign_key (packages/rs-platform-wallet/src/wallet/shielded/sync.rs:826 at PR #4204 head 10514c366) — the L2-invitation claim path's transient scan — streams the on-chain shielded note set from position 0 with no cap (sync_shielded_notes_stream(sdk, &prepared, 0, None)). The only early exit is value coverage (total >= stop_at_value), which a syntactically valid but unfunded invitation key never triggers: one hostile invite forces a genesis-to-tip proof-verify + trial-decrypt scan of the complete shielded history.
The caller's birth-height hint cannot bound this today and is advisory-only (logged at operations.rs:1637): shielded notes are indexed by tree position and the tree exposes no height→position oracle — a chunk's block_height is the proof-tip height, not a per-note inclusion height — so the hint cannot seed the scan start.
Raised as a review suggestion on #4204: #4204 (comment)
Chosen direction
Authenticated funding position carried in the invite payload — a wire-format change to the invitation, which should ride the #4301 two-note invite format change (the natural vehicle for revising the invite payload).
A local scan budget was considered and rejected as the primary fix: a mis-sized bound turns a deep-funded legitimate invite unclaimable, and a caller-supplied birth height is attacker-controlled, so neither bounds hostile input soundly. (A resumable per-attempt scan budget with a typed "budget exhausted, resume to continue" outcome remains a possible interim guard if one is wanted before the wire-format change lands.)
Interim exposure
Bounded operationally rather than in-protocol for now:
- The claim scan is invitee-initiated — an attacker with a hostile invite burns the accepting device's bandwidth/CPU, not the network's.
- The Kotlin host surfaces the claim as a cancellable long-running operation, so a runaway scan is user-abortable.
References
Problem
scan_notes_for_foreign_key(packages/rs-platform-wallet/src/wallet/shielded/sync.rs:826at PR #4204 head10514c366) — the L2-invitation claim path's transient scan — streams the on-chain shielded note set from position 0 with no cap (sync_shielded_notes_stream(sdk, &prepared, 0, None)). The only early exit is value coverage (total >= stop_at_value), which a syntactically valid but unfunded invitation key never triggers: one hostile invite forces a genesis-to-tip proof-verify + trial-decrypt scan of the complete shielded history.The caller's birth-height hint cannot bound this today and is advisory-only (logged at
operations.rs:1637): shielded notes are indexed by tree position and the tree exposes no height→position oracle — a chunk'sblock_heightis the proof-tip height, not a per-note inclusion height — so the hint cannot seed the scan start.Raised as a review suggestion on #4204: #4204 (comment)
Chosen direction
Authenticated funding position carried in the invite payload — a wire-format change to the invitation, which should ride the #4301 two-note invite format change (the natural vehicle for revising the invite payload).
A local scan budget was considered and rejected as the primary fix: a mis-sized bound turns a deep-funded legitimate invite unclaimable, and a caller-supplied birth height is attacker-controlled, so neither bounds hostile input soundly. (A resumable per-attempt scan budget with a typed "budget exhausted, resume to continue" outcome remains a possible interim guard if one is wanted before the wire-format change lands.)
Interim exposure
Bounded operationally rather than in-protocol for now:
References