Expand description
Blacklist compliance watcher (ADR 011 § Content Takedown).
Every paid-delivery node runs this task after resolving the mandatory
blockchain.content_blacklist_address. It keeps the local blob store
compliant with ContentBlacklist: it evicts any blob whose hash is
blacklisted in scope for this operator (global ∪ current-region ∪
ripening-prev-region). The scope decision is the contract’s
isHashBlacklistedForOperator view, so region packing and the ADR 030
ripening math never leave the chain.
Enumerated deny-set, re-scoped. The set of blacklisted entries is read
straight from ContentBlacklist’s enumeration views at one pinned block on
boot, rather than replayed from HashBlacklisted logs (#1497). Two views feed
two sets:
- Hash entries.
getScopeRegions(operator)gives the one-to-three region keys in scope right now; each region’s set is paged fromblacklistedHashes/blacklistedHashCount. Entries are keyed by(region, hash)— the contract’s own key (_hashEntries[region][hash]) — so a same-hash entry under another region survives aHashRemovedfor the first. Every seen-but-not-yet-evicted entry is retained inknownand re-scoped on a periodic (on_tick_complete) pass, because a hash can become live + in scope with noHashBlacklistedevent, via an operator region/ripening change (CapacityBond.updateRegion), which emits nothing on this contract at all. - Address union.
blacklistedAddresses/blacklistedAddressCountenumerate the origin ∪ operator deny set. Each address is liveness-filtered by the contract’s ownisOriginBlacklisted(a) || isOperatorBlacklisted(a)disjunction (the same oneOriginAssignment._syncAddrevaluates), which keeps voted-out OPERATORS (addOperatorsets only the operator mapping) and live origins while dropping emergency origins past their auto-expiry. The survivors seedcrate::content_deny::ContentDenylist, which the delivery path consults to refuse anyStreamRequestfunded by a blacklisted address.
No durable projection, no scan cursor. Both sets are enumerable on chain,
so a restart rebuilds them by reading the chain rather than by reloading a
persisted mirror or replaying from a deploy-block floor. The live tail is
seeded at the enumeration snapshot block (CursorStart::Seeded, no persist) —
there is no historical scan on any boot, warm or cold. This is the same
shape the origin directory and slash watchers already use.
The pinned-block reads are load-bearing, not tidiness: the enumeration views
remove by swap-and-pop, so a page and the count it is checked against MUST be
read at one block height, or a concurrent removal can move an unread element
into an already-read slot and skip it. A seen != count mismatch aborts the
snapshot rather than seating a partial set.
The live tail follows both event families on the shared resumable_watcher
eth_getLogs poller: HashBlacklisted/HashRemoved for the hash set and
OriginBlacklistUpdated + OperatorBlacklisted/OperatorBlacklistCleared
for the address union (both routed to the one deny-set, mirroring
OriginAssignment’s union). It is a low-latency signal to re-read; the
periodic re-enumeration is the backstop that catches anything the tail missed
(a reorg at the tip, or a region/ripening transition into a never-enumerated
region).
Enforcement is two writes per hash, in this order: the deny records that the refusal is governance-sourced, then the eviction reclaims the bytes.
decdn_cache::CacheEngine::evict durably records the takedown (survives
restart via evicted.log) and cascades to every serving surface through
decdn_cache::CacheEngine::refuses: the DHT republisher drops the hash on
its next tick, the probe handler stops signing has_blob: true, and the
client handler never re-pull-fills it. evict is sticky and works on absent
hashes, so a hash blacklisted while the node was offline (and not yet held) is
still pre-blocked.
The deny is what evicted.log cannot express: why. Every one of these
refusals signs the one class Declined, so no client learns which list a
hash is on (ADR 011 §StreamRequest Response); the deny keeps the cause on
the operator’s per-reason metric. The governance deny-set the delivery path reads
(CacheEngine::set_chain_denied_one) is rebuilt each boot by re-checking every
enumerated hash through the same isHashBlacklistedForOperator liveness the
tail uses, so a lapsed entry is not enforced.
A fail-closed readiness gate (InitialSyncGate) keeps every ALPN listener shut
until the first enumeration + enforcement pass completes: a node refuses to
serve until blacklist enforcement is live.
Resilience. The re-scope + re-enumeration pass runs on the operator’s
rescan cadence (checked at the end of every poll tick), pulled forward to the
poll cadence whenever a live re-check fails — an enforcement failure must not
wait out the full cadence while the blob stays slashably servable; every RPC
read is bounded by a per-call timeout; the re-scope is interruptible by
shutdown (checked between hashes) so a large backlog cannot overrun the runtime
shutdown deadline. Serving a blacklisted hash is slashable
(SlashJudge.submitBlacklistChallenge), so prompt eviction is the node’s only
local protection.