Decision 4 of #324. The storage landed in #343 and nothing fills it, which is what this decides.
The direction was agreed — per-list, driven by each list's own NextUpdate, never a global sweep, and a failed refresh leaves the previous good copy in place. put in #343 is already shaped for that. What is not settled is the part below.
What a pass is
A refresh is not one operation. It is:
- fetch the LOTL and verify it against the pinned anchor;
- filter to national pointers —
national_pointers already drops the self-pointer and the PDF variants several Member States publish beside their XML;
- for each pointer, fetch and verify, and write verified or unavailable.
Step 1 failing is categorically different from step 3 failing for one territory: without a verified LOTL there is no authorised certificate list for any national list, so nothing in step 3 can be checked at all. A failed LOTL fetch must not mark 27 territories unavailable — that would turn one outage into a cache that says the Union cannot be checked, and put cannot prevent it because the caller decides.
Open questions
Cadence. Lists carry NextUpdate and the LOTL does too. Per-list scheduling is more faithful and needs per-list timers or a wakeup that scans due rows. A single periodic tick that refreshes whatever is due is far simpler and is what spawn_scan_prune / spawn_idempotency_purge / spawn_job_cleanup already do. The tick is probably right; the due-check needs NextUpdate persisted, which #343 does not store — it was left out deliberately rather than guessed, and this is where it gets decided and added.
Concurrency and politeness. 27+ documents, several MB each, from 27 Member State servers. Sequential is slow and kind; parallel is fast and looks like a small crawl. The guarded outbound path (dpp_common::outbound) is mandatory either way — the LOTL names the URLs, and until it is verified those URLs are attacker-influenceable, which is what fetch_lotl_pointers parsing before verifying already means.
First fill on a fresh node. A node booting with an empty cache has 27 fetches to make and cannot block boot on them. This is the same question as #344 from the other side.
Removal. A territory the LOTL stops naming should eventually leave the cache. #343 grants no DELETE on odal.trusted_list_cache deliberately — adding it means adding the row to ops/pg/README.md's DELETE set, which just grants-check gates. Decide whether stale rows are deleted or simply ignored; ignoring them is safer and leaks rows slowly.
Failure taxonomy. unavailable_reason is free text, and #343 argues that is right because what stops a list being read is not enumerable. But the retry behaviour differs sharply: a fetch timeout should be retried soon, a signature that does not verify should not be hammered, and a document over a parser ceiling (#320, Germany) will never succeed until the parser changes. If retry policy needs to distinguish them, the reason needs structure beside the prose.
The anchor interaction
A refresher is the only thing positioned to act on #321's anchor-freshness signal, and it is also the thing that breaks first when the pin goes stale: the LOTL stops verifying, step 1 fails, and every national list goes unrefreshed. The current signing certificate expires 2027-11-17. See #346.
Not blocking
Nothing here can produce a qualified seal, so an unfilled cache costs nothing today. It becomes the blocker the moment a QTSP credential exists.
Related: #324 · #343 (storage) · #344 (what is reported meanwhile) · #345 (anchor) · #320 (a territory that will not succeed)
Decision 4 of #324. The storage landed in #343 and nothing fills it, which is what this decides.
The direction was agreed — per-list, driven by each list's own
NextUpdate, never a global sweep, and a failed refresh leaves the previous good copy in place.putin #343 is already shaped for that. What is not settled is the part below.What a pass is
A refresh is not one operation. It is:
national_pointersalready drops the self-pointer and the PDF variants several Member States publish beside their XML;Step 1 failing is categorically different from step 3 failing for one territory: without a verified LOTL there is no authorised certificate list for any national list, so nothing in step 3 can be checked at all. A failed LOTL fetch must not mark 27 territories unavailable — that would turn one outage into a cache that says the Union cannot be checked, and
putcannot prevent it because the caller decides.Open questions
Cadence. Lists carry
NextUpdateand the LOTL does too. Per-list scheduling is more faithful and needs per-list timers or a wakeup that scans due rows. A single periodic tick that refreshes whatever is due is far simpler and is whatspawn_scan_prune/spawn_idempotency_purge/spawn_job_cleanupalready do. The tick is probably right; the due-check needsNextUpdatepersisted, which #343 does not store — it was left out deliberately rather than guessed, and this is where it gets decided and added.Concurrency and politeness. 27+ documents, several MB each, from 27 Member State servers. Sequential is slow and kind; parallel is fast and looks like a small crawl. The guarded outbound path (
dpp_common::outbound) is mandatory either way — the LOTL names the URLs, and until it is verified those URLs are attacker-influenceable, which is whatfetch_lotl_pointersparsing before verifying already means.First fill on a fresh node. A node booting with an empty cache has 27 fetches to make and cannot block boot on them. This is the same question as #344 from the other side.
Removal. A territory the LOTL stops naming should eventually leave the cache. #343 grants no DELETE on
odal.trusted_list_cachedeliberately — adding it means adding the row toops/pg/README.md's DELETE set, whichjust grants-checkgates. Decide whether stale rows are deleted or simply ignored; ignoring them is safer and leaks rows slowly.Failure taxonomy.
unavailable_reasonis free text, and #343 argues that is right because what stops a list being read is not enumerable. But the retry behaviour differs sharply: a fetch timeout should be retried soon, a signature that does not verify should not be hammered, and a document over a parser ceiling (#320, Germany) will never succeed until the parser changes. If retry policy needs to distinguish them, the reason needs structure beside the prose.The anchor interaction
A refresher is the only thing positioned to act on #321's anchor-freshness signal, and it is also the thing that breaks first when the pin goes stale: the LOTL stops verifying, step 1 fails, and every national list goes unrefreshed. The current signing certificate expires 2027-11-17. See #346.
Not blocking
Nothing here can produce a qualified seal, so an unfilled cache costs nothing today. It becomes the blocker the moment a QTSP credential exists.
Related: #324 · #343 (storage) · #344 (what is reported meanwhile) · #345 (anchor) · #320 (a territory that will not succeed)