Before this change, calculate_packet_hash() (SHA-256 + hex + upper) was called
3 times per forwarded packet and 4 times per dropped packet:
__call__ → pkt_hash_full = packet.calculate_packet_hash() #1
→ flood/direct_forward → is_duplicate → calculate_packet_hash() #2
→ flood/direct_forward → mark_seen → calculate_packet_hash() #3
(drop) → _get_drop_reason → is_duplicate → calculate_packet_hash() #4
pkt_hash_full was computed in __call__ but never threaded down into
process_packet, flood_forward, direct_forward, is_duplicate, or _get_drop_reason.
Each method recomputed it independently.
Fix: add optional packet_hash: Optional[str] = None to is_duplicate,
_get_drop_reason, flood_forward, direct_forward, and process_packet. Pass
pkt_hash_full from __call__ through the chain. Each method uses the provided
hash or falls back to computing it — preserving backward compatibility for
external callers (TraceHelper, etc.) that have no pre-computed hash.
Result: 1 SHA-256 computation per packet in the hot path regardless of whether
the packet is forwarded or dropped.
Also adds explicit INVARIANT docstrings to flood_forward, direct_forward, and
is_duplicate documenting that these methods must remain synchronous (no await).
The is_duplicate + mark_seen pair is atomic within the asyncio event loop; adding
an await between them would allow two concurrent tasks to both pass the duplicate
check for the same packet — forwarding it twice.
Docs: docs/pr_hash_once.md — problem analysis, call-chain diagram, per-method
diffs, quantification (~3-8 µs saved per packet), test plan (including hash-count
assertion), and proof that passing the original's hash to the deep-copied packet
is correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feat/fix regions: rename global_flood_allow to unscoped_flood_allow,
fix region handling to correctly separate unscoped traffic from scoped
regions, maintain backward compat with existing config files.