Follow-up #3 from `docs/namecoin-spv-follow-up-prs.md`, stacked on top of the `NamecoinTipTracker` chain-walk binding PR. `NamecoinHeaderChainVerifier` now optionally persists its last verified `(tipHeight, tipBlockHashLE)` across process cold starts. Warm starts restore the cache lazily and walk only the delta since the previous run; cold starts continue to walk from the pinned checkpoint. ## Interface ```kotlin interface ChainVerifierCachePersistence { suspend fun load(): PersistedTip? suspend fun save(entry: PersistedTip) } data class PersistedTip( val checkpointHeight: Int, val checkpointBlockHashLE: ByteArray?, val tipHeight: Int, val tipBlockHashLE: ByteArray, ) ``` Both live in commonMain so the walker stays multiplatform-agnostic; production Android callers plug in a SharedPreferences-backed implementation, jvmTest a fake, and unset callers keep the pre-PR in-memory-only behaviour. `NamecoinHeaderChainVerifier` gains one new constructor param: ```kotlin private val persistence: ChainVerifierCachePersistence? = null, ``` Null is the default โ€” every existing call site keeps working unchanged. ## Semantics **Lazy one-shot load.** The walker calls `load()` at most once per instance, from inside its internal mutex on the first `verifyToTip()` that reaches the walk path. No I/O in the constructor, no repeated loads across subsequent walks, no separate synchronisation contract for the implementation. **Save after every advancing walk.** After a successful walk that updates the in-memory cache, the walker calls `save(entry)` with the current checkpoint identity plus the new tip. Callers that ask for a tip already covered by the in-memory cache do NOT trigger a save. **Automatic invalidation on checkpoint change.** The walker refuses to restore a persisted entry whose `(checkpointHeight, checkpointBlockHashLE)` does not match its own. A checkpoint bump in a new release transparently forces a fresh walk from the new pin; implementations do not need to (and MUST NOT) do any invalidation of their own. **Hostile-store hardening.** Entries whose `tipHeight < checkpoint.height` or whose `tipBlockHashLE.size != 32` are rejected without ever seeding the in-memory cache. A corrupted or malicious store cannot inject a bogus tip; the worst it can do is force a cold-start walk. **Failure-swallowing.** Both `load()` and `save()` are wrapped so that any `Throwable` (except `CancellationException`) is caught and discarded. Warm-start caching is a performance optimisation, never a correctness dependency โ€” a failing store means "walk from checkpoint this time" and "next cold start re-walks from checkpoint", nothing worse. ## Tests `NamecoinHeaderChainVerifierTest.kt` gains 10 hermetic tests covering: - Cold-start save writes the correct `(checkpointHeight, checkpointBlockHashLE, tipHeight, tipBlockHashLE)` tuple. - Warm-start restore skips all header fetches when the persisted tip matches the request. - Warm-start delta walk fetches only headers past the persisted tip and re-saves the advanced tuple. - Persisted entry with mismatched `checkpointHeight` is dropped and overwritten. - Persisted entry with mismatched `checkpointBlockHashLE` is dropped and overwritten. - Persisted entry with `tipHeight < checkpoint.height` (hostile store) is dropped without seeding the cache. - `load()` throwing does not break the walk (save still happens). - `save()` throwing does not break the walk. - `load()` is called exactly once across multiple `verifyToTip()` invocations on the same instance. - Null `persistence` preserves pre-PR in-memory-only behaviour. Full suite still green (`:quartz:jvmTest -Dnamecoin.live=false`, BUILD SUCCESSFUL in 2m 55s, no regressions). ## What this closes from the RFC The RFC's follow-up series (`docs/namecoin-spv-follow-up-prs.md`) had three ordered PRs after the initial walker landed: 1. AuxPoW-aware header verifier โ€” landed. 2. `NamecoinTipTracker` integration โ€” landed. 3. Persisted `(height, hash)` cache โ€” this PR. With this in, the SPV series is functionally complete. Remaining work is release-process (`NamecoinCheckpoint` advancement, `DEFAULT_CHECKPOINT_BLOCK_HASH_LE` pinning) plus the `get_value_proof_transitive` protocol RFC in `namecoin/electrumx`, none of which block this repo.
โ†‘