ensureFreshMerkleProofPath returns the fetched path instead of leaving
callers to re-read merkleProofCache. generateProof uses the returned
value, so an invalidate landing during the refresh's updateMemberCache
suspension can no longer empty the cache under an in-flight proof-gen
and trip its empty-cache guard. The cache field is now only a hint for
the next call.
invalidateMerkleProofCache also bumps merkleProofCacheGeneration. The
refresh stamps the generation before each fetchMerkleProofElements and
only caches a result whose generation is unchanged, otherwise it fetches
again, bounded by MerkleProofRefetchMaxAttempts. A rejection-driven
invalidate is therefore never absorbed by a fetch that predates it, so
the path a caller receives always postdates the invalidate.
refreshRoots awaits rootsRefreshInFlightFut via join() at both await
sites, giving the roots refresh the same cancellation shield the proof
path refetch already had: a cancelled validateRoot caller no longer
cancels the refresh its coalesced peers are waiting on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>