Gas DoS via Key Chain Traversal
getLatestChainEntry runs two O(N) traversals plus a fixed 1000-element memory allocation on every call, costing ~900k gas after only 50 rotations and growing unboundedly toward the block gas limit.
Description
getLatestChainEntry calls buildFromPublicKey → _buildChainFromHash,
which performs two O(N) traversals every time it is called:
- Backward traversal (lines 261-282): follows
prevPublicKeyHashlinks to find inception. - Forward traversal (lines 289-303): follows
prerotatedKeyHashlinks from inception to the latest entry.
Additionally, _buildChainFromHash allocates a fixed KeyLogEntry[](1000)
array in memory on every call (line 253), even though the Bridge only ever
needs the latest entry (log[log.length - 1]). The full array is
never consumed by any on-chain caller, Bridge.sol calls only
getLatestChainEntry.
Measured cost: a single getLatestChainEntry call costs between
700k–900k gas at chain length 101 when called from the last key (key chain
length: each pair rotation adds 2 entries, so 50 rotations = chain length
101 including inception). The cost depends on the starting key's position
in the chain. The Bridge passes ctx.unconfirmed.publicKey, a key near
the end, requiring near-full backward traversal before the forward
traversal.
Vulnerable Scenario
- A user registers key inception via
registerKeyLog, chain length is now 1. - The user performs 50 key pair rotations via
registerKeyLogPair, each rotation adds 2 entries (unconfirmed + confirming), growing the chain to 101 entries. - The user calls any bridge operation (e.g.,
registerKeyPairWithTransferto wrap tokens). Internally, the Bridge callsgetLatestChainEntry(ctx.unconfirmed.publicKey)(line 512). _buildChainFromHashreceives the unconfirmed key (near the end of the chain) and:- Traverses backward through ~100 entries to find inception (~100 SLOADs)
- Allocates
KeyLogEntry[](1000)in memory - Traverses forward through all 100 entries to find the latest (~100 SLOADs)
- Allocates a result array and copies all entries
- Total cost: ~900k gas for a single lookup, just to retrieve the latest entry.
- With continued rotations, gas cost grows by ~4,400 per additional entry, eventually making bridge operations impractical or exceeding the block gas limit.
Impact
- Every bridge operation (wrap, unwrap, transfer, key rotation) pays O(N) gas for key chain lookup.
- Users with long key chains eventually cannot perform any operations.
- If the owner's chain grows too long, the bridge becomes unmanageable.
- At ~1000 entries, chains hit the hardcoded array limit and silently truncate (see [F-2026-0014]).
Recommendation
Maintain two index mappings, updated during
registerKeyLog / registerKeyLogPair, to make all lookups O(1):
mapping(address => address) public chainOf; // any key hash → inception hashmapping(address => address) public latestInChain; // inception hash → latest key hash
This eliminates both traversals: given any mid-chain key, chainOf
resolves to inception in O(1), and latestInChain resolves to the latest
entry in O(1). The fixed 1000-element array allocation also becomes
unnecessary.
Resolution
YadaCoin, Confirmed.
Zealynx, Fixed.

