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.
Login to reply